0
|
1 #! @PERL@ -wT |
|
2 |
|
3 # get local DCC parameters for DCC whitelist CGI scripts. |
|
4 |
|
5 # Copyright (c) 2008 by Rhyolite Software, LLC |
|
6 # |
|
7 # This agreement is not applicable to any entity which sells anti-spam |
|
8 # solutions to others or provides an anti-spam solution as part of a |
|
9 # security solution sold to other entities, or to a private network |
|
10 # which employs the DCC or uses data provided by operation of the DCC |
|
11 # but does not provide corresponding data to other users. |
|
12 # |
|
13 # Permission to use, copy, modify, and distribute this software without |
|
14 # changes for any purpose with or without fee is hereby granted, provided |
|
15 # that the above copyright notice and this permission notice appear in all |
|
16 # copies and any distributed versions or copies are either unchanged |
|
17 # or not called anything similar to "DCC" or "Distributed Checksum |
|
18 # Clearinghouse". |
|
19 # |
|
20 # Parties not eligible to receive a license under this agreement can |
|
21 # obtain a commercial license to use DCC by contacting Rhyolite Software |
|
22 # at sales@rhyolite.com. |
|
23 # |
|
24 # A commercial license would be for Distributed Checksum and Reputation |
|
25 # Clearinghouse software. That software includes additional features. This |
|
26 # free license for Distributed ChecksumClearinghouse Software does not in any |
|
27 # way grant permision to use Distributed Checksum and Reputation Clearinghouse |
|
28 # software |
|
29 # |
|
30 # THE SOFTWARE IS PROVIDED "AS IS" AND RHYOLITE SOFTWARE, LLC DISCLAIMS ALL |
|
31 # WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES |
|
32 # OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL RHYOLITE SOFTWARE, LLC |
|
33 # BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES |
|
34 # OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, |
|
35 # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, |
|
36 # ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS |
|
37 # SOFTWARE. |
|
38 # Rhyolite Software DCC 1.3.103-1.89 $Revision$ |
|
39 # @configure_input@ |
|
40 |
|
41 # check this file by running it separately |
|
42 use strict 'subs'; |
|
43 |
|
44 use integer; |
|
45 |
|
46 use 5.004; |
|
47 use Fcntl qw(:DEFAULT :flock); |
|
48 use POSIX qw(strftime); |
|
49 |
|
50 # quiet Perl taint checks with a path that should work everywhere for |
|
51 # the few commands these scripts use. |
|
52 $ENV{PATH}="/sbin:/bin:/usr/sbin:/usr/bin"; |
|
53 |
|
54 # global variables |
|
55 # $DCCM_USERDIRS, # from dcc_conf |
|
56 # $whiteclnt, # path to the per-user whitelist file |
|
57 # %query, |
|
58 # $thold_cks, # checksums that can have thresholds |
|
59 # $user, |
|
60 # $hostname, |
|
61 # $user_dir, |
|
62 # $edit_url, |
|
63 # $list_log_url, |
|
64 # $list_log_link, |
|
65 # $list_msg_link, |
|
66 # $edit_url, $edit_link, |
|
67 # $passwd_url, $passwd_link, |
|
68 # $logoutID, |
|
69 # $url_ques, $url_suffix, |
|
70 # $sub_white, # 'subsitute' headers from dcc_conf |
|
71 # $form_hidden # state for main form |
|
72 |
|
73 |
|
74 |
|
75 # so this file can be used with do('@cgibin@/common') |
|
76 # besides, check_user() must be called before html_head() |
|
77 return check_user(); |
|
78 |
|
79 |
|
80 |
|
81 sub debug_time { |
|
82 my($label) = @_; |
|
83 |
|
84 return if (!$query{debug}); |
|
85 |
|
86 my(@ts, $ts); |
|
87 require 'sys/syscall.ph'; |
|
88 |
|
89 $ts = pack("LL", ()); |
|
90 syscall(&SYS_gettimeofday, $ts, 0); |
|
91 @ts = unpack("LL", $ts); |
|
92 |
|
93 chomp($label); |
|
94 printf STDERR "%38s", $label; |
|
95 print STDERR strftime(" %X", localtime($ts[0])); |
|
96 printf STDERR ".%03d", $ts[1]/1000; |
|
97 printf STDERR " %.3f", $_ foreach times; |
|
98 print STDERR "\n"; |
|
99 } |
|
100 |
|
101 |
|
102 |
|
103 sub debug_printf { |
|
104 my($label, $str) = @_; |
|
105 |
|
106 return if (!$query{debug}); |
|
107 $str =~ s/\n/\\n/g; |
|
108 print STDERR "$label='$str'\n"; |
|
109 } |
|
110 |
|
111 |
|
112 # emit HTTP/HTML header |
|
113 sub html_head { |
|
114 my($title, # title of the web page |
|
115 $refresh_url) = @_; # next step in re-login sequence if not null |
|
116 my($header, $style); |
|
117 |
|
118 print <<EOF; |
|
119 Content-type: text/html; charset=iso-8859-1 |
|
120 Expires: Thu, 01 Dec 1994 16:00:00 GMT |
|
121 pragma: no-cache |
|
122 |
|
123 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> |
|
124 <HTML> |
|
125 <HEAD> |
|
126 <TITLE>$title</TITLE> |
|
127 <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1"> |
|
128 <META HTTP-EQUIV="Content-Style-Type" CONTENT="text/css"> |
|
129 EOF |
|
130 |
|
131 print "<META HTTP-EQUIV=refresh content=\"1;url=$refresh_url\">\n" |
|
132 if ($refresh_url); |
|
133 |
|
134 # Use header if supplied |
|
135 # it is mostly text for the start of the <BODY>, |
|
136 # but it can also contain either <LINK rel="stylesheet"... |
|
137 # or <STYLE>...</STYLE> |
|
138 $header = "\n"; |
|
139 if (open(HEADER, "$user_dir/header") |
|
140 || open(HEADER, "@cgibin@/header")) { |
|
141 my $line; |
|
142 |
|
143 $header .= $line while ($line = <HEADER>); |
|
144 close(HEADER); |
|
145 } |
|
146 |
|
147 # Use our style style if the supplied header has none |
|
148 if ($header =~ s/([ \t]*<STYLE[^>]*>.*<\/STYLE>\s*)//si) { |
|
149 $style = $1; |
|
150 } elsif ($header =~ s/([ \t]*<link[^>]*rel=['"]?stylesheet[^>]*>)//si) { |
|
151 $style = $1; |
|
152 } else { |
|
153 $style = <<EOF; |
|
154 <STYLE type="text/css"> |
|
155 <!-- |
|
156 BODY {background-color:white; color:black} |
|
157 .warn {color:red} |
|
158 .mono {font-family:monospace} |
|
159 .small {font-size:smaller} |
|
160 .strong {font-weight:bolder} |
|
161 .nopad {margin:0; padding:0} |
|
162 INPUT.selected {font-size:smaller; font-weight:bolder; color:blue} |
|
163 TABLE {white-space:nowrap} |
|
164 TD.first {text-align:right; vertical-align:baseline} |
|
165 IMG.logo {width:6em; vertical-align:middle} |
|
166 ADDRESS {font-size:smaller} |
|
167 --> |
|
168 </STYLE> |
|
169 EOF |
|
170 } |
|
171 |
|
172 print <<EOF; |
|
173 $style |
|
174 </HEAD> |
|
175 <BODY> |
|
176 <H2>$title</H2> |
|
177 $header |
|
178 EOF |
|
179 } |
|
180 |
|
181 |
|
182 |
|
183 sub html_footer { |
|
184 if (open(FOOTER, "$user_dir/footer") |
|
185 || open(FOOTER, "@cgibin@/footer")) { |
|
186 my $line; |
|
187 |
|
188 print $line while ($line = <FOOTER>); |
|
189 close(FOOTER); |
|
190 } |
|
191 } |
|
192 |
|
193 |
|
194 |
|
195 sub common_buttons { |
|
196 my($msg, $cur, $list_log, $edit, $passwd, $id); |
|
197 |
|
198 |
|
199 $msg = $query{msg} ? "${url_ques}msg=$query{msg}" : ""; |
|
200 |
|
201 $cur = "$ENV{SCRIPT_NAME}$url_suffix"; |
|
202 $list_log = ($cur ne $list_log_url |
|
203 ? "$list_log_link$msg\">Log</A>" |
|
204 : "List Log"); |
|
205 $edit = ($cur ne $edit_url |
|
206 ? "$edit_link\">Settings</A>" |
|
207 : "Settings"); |
|
208 $passwd = ($cur ne $passwd_url |
|
209 ? "$passwd_link\">Password</A>" |
|
210 : "Password"); |
|
211 |
|
212 print <<EOF; |
|
213 <TABLE> |
|
214 <TR><TD>$list_log |
|
215 <TD>$edit |
|
216 <TD>$passwd |
|
217 <TD><A HREF="$cur${url_ques}logoutID=$logoutID">LogOut/In</A> |
|
218 EOF |
|
219 } |
|
220 |
|
221 |
|
222 |
|
223 # give up, but not entirely, with an HTML whine |
|
224 sub html_whine { |
|
225 my($msg) = @_; |
|
226 |
|
227 html_head("Internal Error"); |
|
228 common_buttons(); |
|
229 print <<EOF; |
|
230 </TABLE> |
|
231 <H1>Internal Error</H1> |
|
232 <P class=warn>$msg |
|
233 <P><HR> |
|
234 $ENV{SERVER_SIGNATURE} |
|
235 </BODY> |
|
236 </HTML> |
|
237 EOF |
|
238 exit; |
|
239 } |
|
240 |
|
241 |
|
242 |
|
243 # die with an HTML whine |
|
244 sub html_die { |
|
245 my($msg) = @_; |
|
246 |
|
247 # put the message into the httpd error log |
|
248 print STDERR "DCC CGI script internal error: $msg\n"; |
|
249 |
|
250 html_head("Internal Error"); |
|
251 print <<EOF; |
|
252 <P class=warn>$msg |
|
253 <P><HR> |
|
254 $ENV{SERVER_SIGNATURE} |
|
255 </BODY> |
|
256 </HTML> |
|
257 EOF |
|
258 exit; |
|
259 } |
|
260 |
|
261 |
|
262 # punt to some other web page, perhaps after the logout/in kludge |
|
263 # this cannot be used after html_head() |
|
264 sub punt2 { |
|
265 my($msg, # message saying why |
|
266 $url) = @_; # the other web page |
|
267 |
|
268 # don't punt a punt |
|
269 html_die($msg) if ($query{result}); |
|
270 |
|
271 $url = ((($ENV{HTTPS} && $ENV{HTTPS} eq "on") ? "https://" : "http://") |
|
272 . $ENV{SERVER_NAME} |
|
273 . $url); |
|
274 $url .= $url_ques."result=".url_encode($msg) if ($msg); |
|
275 |
|
276 print "Status: 302 Moved Temporarily\nLocation: $url\n"; |
|
277 html_head("redirect to $url"); |
|
278 print "redirecting to $url\n</BODY>\n</HTML>\n"; |
|
279 exit; |
|
280 } |
|
281 |
|
282 |
|
283 |
|
284 # Check authentication and gather system parameters. |
|
285 # Require a user name as well as one that can't be used as a sneaky path. |
|
286 sub check_user { |
|
287 my($sub_args, $cks, $thold, $line, $var); |
|
288 |
|
289 if ($ENV{HTTP_NAME}) { |
|
290 $hostname = $ENV{HTTP_NAME}; |
|
291 } elsif ($ENV{SERVER_NAME}){ |
|
292 $hostname = $ENV{SERVER_NAME}; |
|
293 } else { |
|
294 $hostname=`hostname`; |
|
295 chop($hostname); |
|
296 } |
|
297 |
|
298 $user = $ENV{REMOTE_USER}; |
|
299 if (!$user){ |
|
300 $user = ''; |
|
301 html_die("no user name") |
|
302 } |
|
303 # allow the user name to be a subdirectory |
|
304 html_die("user name $user is invalid") |
|
305 if ($user =~ /\.\./ || $user !~ /^([-\/.,#_%a-z0-9]+)$/i); |
|
306 $user = $1; # stop Perl taint warnings |
|
307 |
|
308 # convert the user name to lower case because sendmail likes to |
|
309 $user =~ tr/A-Z/a-z/; |
|
310 |
|
311 # rely on the /var/dcc/dcc_conf configuration file for almost everything |
|
312 $DCC_HOMEDIR = "@prefix@"; # unneeded except for compatibility |
|
313 $DCCM_USERDIRS = "userdirs"; |
|
314 $DCCM_ENABLE = "on"; |
|
315 $DCCIFD_ENABLE = "off"; |
|
316 open(CONF, '2>/dev/null sh -c \'. @prefix@/dcc_conf; |
|
317 echo DCCM_ENABLE="$DCCM_ENABLE"; |
|
318 echo DCCM_USERDIRS="$DCCM_USERDIRS"; |
|
319 echo DCCM_ARGS="$DCCM_ARGS"; |
|
320 echo DCCM_REJECT_AT="$DCCM_REJECT_AT"; |
|
321 echo DCCM_CKSUMS="$DCCM_CKSUMS"; |
|
322 echo DCCIFD_USERDIRS="$DCCIFD_USERDIRS"; |
|
323 echo DCCIFD_ENABLE="$DCCIFD_ENABLE"; |
|
324 echo DCCIFD_ARGS="$DCCIFD_ARGS"; |
|
325 echo DCCIFD_REJECT_AT="$DCCIFD_REJECT_AT"; |
|
326 echo DCCIFD_CKSUMS="$DCCIFD_CKSUMS"; |
|
327 echo GREY_CLIENT_ARGS="$GREY_CLIENT_ARGS"; |
|
328 echo DNSBL_ARGS="$DNSBL_ARGS"; |
|
329 \'|') |
|
330 || html_die("cannot get DCC configuration"); |
|
331 while ($line = <CONF>) { |
|
332 chomp($line); |
|
333 if ($line !~ s/(^[A-Z_]+)=//) { |
|
334 print STDERR "unrecognized dcc_conf line $line"; |
|
335 next; |
|
336 } |
|
337 $var = $1; |
|
338 if ($line =~ /^([-0-9,.\/a-z_]*)$/i) { |
|
339 ${$var} = $1; # suppress taint warnings on good paths |
|
340 } else { |
|
341 ${$1} = $line; |
|
342 } |
|
343 } |
|
344 close(CONF); |
|
345 |
|
346 $main_whiteclnt = "@prefix@/whiteclnt"; |
|
347 if ($DCCM_ENABLE eq "off" && $DCCIFD_ENABLE eq "on") { |
|
348 $sub_args = $DCCIFD_ARGS; |
|
349 $cks = $DCCIFD_CKSUMS; |
|
350 $thold = $DCCIFD_REJECT_AT; |
|
351 $logout_tmpdir = "@prefix@/$DCCIFD_USERDIRS/tmp"; |
|
352 # Assume "name" per-user directory for simple dccifd user names. |
|
353 $user_dir = "@prefix@/$DCCIFD_USERDIRS/$user"; |
|
354 } else { |
|
355 $sub_args = $DCCM_ARGS; |
|
356 $cks = $DCCM_CKSUMS; |
|
357 $thold = $DCCM_REJECT_AT; |
|
358 $logout_tmpdir = "@prefix@/$DCCM_USERDIRS/tmp"; |
|
359 # Assume "local/name" per-user directory for simple dccm user names. |
|
360 $user_dir = ($user =~ /\//) ? $user : "local/$user"; |
|
361 $user_dir = "@prefix@/$DCCM_USERDIRS/$user_dir"; |
|
362 } |
|
363 html_die("no user directory $user_dir") |
|
364 if (! -d $user_dir) ; |
|
365 $logdir = "$user_dir/log"; |
|
366 $whiteclnt = "$user_dir/whiteclnt"; |
|
367 |
|
368 # Figure out which substitute headers are turned on |
|
369 # This does not detect all possible SMTP "field names," but it also |
|
370 # won't get Perl confused with field names such as 'foo[bar]'. |
|
371 $sub_hdrs = ""; |
|
372 $sub_hdrs .= "|$1" |
|
373 while ($sub_args && $sub_args =~ s/(?:-[VdbxANQW]*S\s*) |
|
374 ((?i:[-a-z_0-9]+)) |
|
375 ($|\s+) |
|
376 /$2/x); |
|
377 $sub_white = $sub_hdrs; |
|
378 # pattern matching optional or substitute SMTP headers |
|
379 $sub_hdrs =~ s/^\|+//; |
|
380 # pattern matching optional or substitute checksum types |
|
381 $sub_white =~ s/\|/)|(substitute\\s+/g; |
|
382 $sub_white =~ s/^[|)(]*/(/; |
|
383 $sub_white .= ')'; |
|
384 |
|
385 # names of checksums whose thresholds can be set |
|
386 $thold_cks_cmn = 'Body,Fuz1,Fuz2'; |
|
387 $thold_cks = $thold_cks_cmn; |
|
388 # all checksums including those not kept by (almost all) DCC servers |
|
389 #$thold_cks_all = 'IP,env_From,From,env_To,Message-ID,' . $thold_cks; |
|
390 |
|
391 # compute default checksum thresholds |
|
392 if ($thold) { |
|
393 $cks = $thold_cks_cmn if (!$cks); |
|
394 foreach my $ck (split(/,/,$cks)) { |
|
395 my ($t,$v) = ($ck, $thold); |
|
396 $conf_cks_tholds{$t} = "<STRONG>$v</STRONG> <SMALL>by default in @prefix@/dcc_conf</SMALL>" |
|
397 if (parse_thold_value($t, $v)); |
|
398 } |
|
399 } |
|
400 |
|
401 $cgibin = $ENV{SCRIPT_NAME}; |
|
402 # trim the name of our script from the path |
|
403 $cgibin =~ s!/+[^/]+$!!; |
|
404 # trim extra leading /s that can mess up our generated links |
|
405 $cgibin =~ s!^/{2,}!/!; |
|
406 |
|
407 get_query(); |
|
408 |
|
409 return 1; |
|
410 } |
|
411 |
|
412 |
|
413 |
|
414 # Get user's parameters |
|
415 sub get_query { |
|
416 my($buffer, $name, $value); |
|
417 |
|
418 if ($ENV{REQUEST_METHOD} && $ENV{REQUEST_METHOD} =~ /GET|HEAD/) { |
|
419 $buffer = $ENV{'QUERY_STRING'}; |
|
420 } elsif (!$ENV{CONTENT_LENGTH}) { |
|
421 $buffer = ''; |
|
422 } else { |
|
423 read(STDIN, $buffer, $ENV{CONTENT_LENGTH}); |
|
424 } |
|
425 $buffer =~ tr/+/ /; |
|
426 foreach my $pair (split(/&/, $buffer)) { |
|
427 ($name, $value) = split(/=/, $pair); |
|
428 $name =~ s/%([a-fA-F0-9]{2})/pack("C", hex($1))/eg; |
|
429 if ($value) { |
|
430 $value =~ s/%([a-fA-F0-9]{2})/pack("C", hex($1))/eg; |
|
431 } else { |
|
432 $value = ""; |
|
433 } |
|
434 $query{$name} = $value; |
|
435 } |
|
436 |
|
437 if (!$query{debug} || $query{debug} !~ /^\d+$/) { |
|
438 $url_ques = "?"; |
|
439 $url_suffix = ""; |
|
440 $form_hidden = ""; |
|
441 } else { |
|
442 if ($query{debug} > 1) { |
|
443 debug_time("start $ENV{SCRIPT_NAME}"); |
|
444 print STDERR " $_=\"$query{$_}\"\n" foreach (keys %query); |
|
445 print STDERR " AuthName=\"$ENV{AuthName}\"\n" |
|
446 if ($ENV{AuthName}); |
|
447 print STDERR " SCRIPT_NAME=\"$ENV{SCRIPT_NAME}\"\n" |
|
448 if ($ENV{SCRIPT_NAME}); |
|
449 } |
|
450 $url_suffix = "?debug=$query{debug}"; |
|
451 $url_ques = '&'; |
|
452 $form_hidden = "\n <INPUT type=hidden name=debug value=$query{debug}>"; |
|
453 } |
|
454 |
|
455 $list_log_url = "$cgibin/list-log$url_suffix"; |
|
456 $list_log_link = "<A HREF=\"$list_log_url"; |
|
457 $list_msg_link = "<A HREF=\"$cgibin/list-msg$url_suffix"; |
|
458 $edit_url = "$cgibin/edit-whiteclnt$url_suffix"; |
|
459 $edit_link = "<A HREF=\"$edit_url"; |
|
460 $passwd_url = "$cgibin/chgpasswd$url_suffix"; |
|
461 $passwd_link = "<A HREF=\"$passwd_url${url_ques}goback=$ENV{SCRIPT_NAME}"; |
|
462 |
|
463 $logoutID = $ENV{UNIQUE_ID}; |
|
464 # do the best we can if Apache mod_unique_id is not present |
|
465 $logoutID = "$ENV{REMOTE_ADDR}-$ENV{REMOTE_PORT}-$$-" . time() |
|
466 if (!$logoutID); |
|
467 $logoutID = url_encode($logoutID); |
|
468 |
|
469 # kludge to handle "logout" button including recognizing that we have |
|
470 # already handled it. The usual tactic of requiring the user to |
|
471 # specifying a new username and then using a cookie seems ugly. |
|
472 $tfile = $query{logoutID}; |
|
473 if ($tfile && $tfile =~ /^([-.A-Za-z0-9@]+)$/) { |
|
474 $tfile = "$logout_tmpdir/logout.$1"; |
|
475 |
|
476 # delete any old logout marker files |
|
477 my($old_tfiles) = `find $logout_tmpdir -name 'logout.*' -mtime +1`; |
|
478 if ($old_tfiles && $old_tfiles =~ /^(.+)\s*$/) { |
|
479 $old_tfiles = $1; # untaint |
|
480 my @old_tfiles = split /\s/,$old_tfiles; |
|
481 print "unlink($old_tfiles): $!\n" |
|
482 if ($#old_tfiles >= unlink @old_tfiles); |
|
483 } |
|
484 |
|
485 # Look for our logout marker file. |
|
486 if (-f $tfile) { |
|
487 # If it exists, then we have been here before, so just delete it. |
|
488 # and refresh |
|
489 unlink $tfile; |
|
490 punt2("", "$ENV{SCRIPT_NAME}$url_suffix"); |
|
491 |
|
492 } else { |
|
493 # If it does not exist, create it & force a cycle of authentication. |
|
494 if (!open(TFILE, "> $tfile")) { |
|
495 print STDERR "open($tfile): $!\n"; |
|
496 html_whine("open($tfile): $!", $edit_url); |
|
497 } |
|
498 while (($name,$value) = each %ENV) { |
|
499 print TFILE "$name=$value\n"; |
|
500 } |
|
501 |
|
502 # Demand a new user name and password |
|
503 my($AuthName) = $ENV{AuthName} ? $ENV{AuthName} : "DCC user"; |
|
504 print <<EOF; |
|
505 WWW-authenticate: Basic realm="$AuthName" |
|
506 Status: 401 Unauthorized |
|
507 EOF |
|
508 html_head("Access Failure"); |
|
509 print "<P class=warn>\n"; |
|
510 print $msg ? $msg : "Access Failure"; |
|
511 print "\n</BODY></HTML>\n"; |
|
512 exit; |
|
513 } |
|
514 } |
|
515 } |
|
516 |
|
517 |
|
518 |
|
519 |
|
520 ########################################################################## |
|
521 |
|
522 # %-encode text for a URL |
|
523 sub url_encode { |
|
524 my($out) = @_; |
|
525 |
|
526 $out =~ s/([^-_.+!*(),0-9a-zA-Z])/sprintf("%%%02X",ord($1))/eg; |
|
527 return $out; |
|
528 } |
|
529 |
|
530 |
|
531 |
|
532 # encode text for ordinary HTML to avoid special HTML flags such as '<' |
|
533 # retain newlines |
|
534 sub html_str_encode { |
|
535 my($out) = @_; |
|
536 |
|
537 $out =~ s/&/&/g; |
|
538 $out =~ s/</</g; |
|
539 $out =~ s/>/>/g; |
|
540 $out =~ s/([\00-\10\13-\17\42\47\177-\377])/sprintf("&#%d;",ord($1))/eg; |
|
541 return $out; |
|
542 } |
|
543 |
|
544 |
|
545 |
|
546 # encode text for HTML, and replace newlines with <BR> |
|
547 sub html_text_encode { |
|
548 my($out) = html_str_encode(@_); |
|
549 $out =~ s/\n/<BR>\n/g; |
|
550 return $out; |
|
551 } |
|
552 |
|
553 |
|
554 |
|
555 # encode text for HTML, trimmed to at most 32 characters with the end replaced |
|
556 # by an ellipsis if too long |
|
557 sub hdr_trim_encode { |
|
558 my($out) = @_; |
|
559 |
|
560 return " " if (!$out); |
|
561 |
|
562 return html_str_encode($out) if (length($out) <= 32); |
|
563 |
|
564 $out = substr($out, 0, 28) |
|
565 if ($out !~ s/(^.{20,28}[^<>.@\t ])[<>.@\t ].*/$1/); |
|
566 $out = html_str_encode($out); |
|
567 $out .= " ..."; |
|
568 return $out; |
|
569 } |
|
570 |
|
571 |
|
572 |
|
573 ########################################################################## |
|
574 # Open and parse a log message |
|
575 # sets these globals |
|
576 # $msg_date # envelope |
|
577 # $msg_helo # envelope |
|
578 # $msg_ip # envelope |
|
579 # $msg_client_name # envelope |
|
580 # $msg_env_from # envelope |
|
581 # @msg_env_to # envelope |
|
582 # $msg_mail_host # envelope |
|
583 # $msg_from # header |
|
584 # $msg_subject |
|
585 # $msg_hdrs |
|
586 # $msg_body |
|
587 # $msg_cksums |
|
588 # $msg_result |
|
589 |
|
590 |
|
591 # globals |
|
592 # %msgs_cache, # key=compressed name, [0]=mtime [1]=i-number |
|
593 # $cache_line_len, |
|
594 # $cache_pack, |
|
595 # $cache_version, |
|
596 # $msg_encode_str, |
|
597 # %msgs_cache_state, # key=day, value=1 if good |
|
598 # %msgs_date, %msgs_result, |
|
599 # %msgs_from, %msgs_subject, |
|
600 # $msg_day_first, $msg_day_last, |
|
601 # $msg_first, $msg_last, |
|
602 # $msg_newer, $msg_part_num, |
|
603 # @msgs_num # compressed names sorted by mtime |
|
604 |
|
605 |
|
606 sub parse_log_msg { |
|
607 my($msg, $no_body) = @_; |
|
608 my(@error, $path, $line, $num_hdrs, $cur_hdr, $hdr_type, |
|
609 $misc_hdr, $seen_message_id, $ise_msg, $cksum_marker, $cksum_marker_p); |
|
610 |
|
611 undef $msg_date; |
|
612 undef $msg_helo; |
|
613 undef $msg_ip; |
|
614 undef $msg_client_name; |
|
615 undef $msg_env_from; |
|
616 undef @msg_env_to; |
|
617 undef $msg_mail_host; |
|
618 undef $msg_from; |
|
619 undef $msg_subject; |
|
620 $msg_hdrs = ''; |
|
621 $msg_body = ''; |
|
622 $msg_cksums = ''; |
|
623 $msg_result = ': '; |
|
624 |
|
625 $num_hdrs = 0; |
|
626 |
|
627 $ise_msg = "Internal Server Error"; |
|
628 $cksum_marker = "### end of message body ########################\n"; |
|
629 $cksum_marker_p = qr/^### end of message body ########################\s*$/; |
|
630 |
|
631 $no_body = "" if ($no_body && $no_body ne "no body"); |
|
632 $path = msg2path($msg); |
|
633 |
|
634 sysopen(MSG, $path, O_RDONLY, 0) |
|
635 || return ($ise_msg, "open($path): $!"); |
|
636 |
|
637 return ($ise_msg, "empty $path") if (!($msg_date = <MSG>)); |
|
638 |
|
639 if ($msg_date !~ /^VERSION/) { |
|
640 close(MSG); |
|
641 return ($ise_msg, "format of $path unrecognized"); |
|
642 } |
|
643 if (!($msg_date = <MSG>)) { |
|
644 close(MSG); |
|
645 return ($ise_msg, "$path truncated after VERSION line"); |
|
646 } |
|
647 if (!($msg_date =~ s/^DATE: +(.*) +[^ ]+/$1/)) { |
|
648 close(MSG); |
|
649 return ($ise_msg, "unrecognized DATE line $msg_date in message $msg"); |
|
650 } |
|
651 |
|
652 if (!($msg_ip = <MSG>)) { |
|
653 close(MSG); |
|
654 return ($ise_msg, "message $msg truncated in envelope"); |
|
655 } |
|
656 if ($msg_ip =~ /^IP: ([^ :]*) *([:.0-9a-fA-F]*) *$/) { |
|
657 $msg_ip = $2; |
|
658 $msg_client_name = $1; |
|
659 $msg_ip =~ s/^::ffff://i; |
|
660 $msg_client_name =~ s/^\[.*]$//; |
|
661 $msg_client_name = ' ' if ($msg_client_name eq ''); |
|
662 if (!($msg_helo = <MSG>)) { |
|
663 close(MSG); |
|
664 return ($ise_msg, "message $msg truncated in envelope"); |
|
665 } |
|
666 chop($msg_helo); |
|
667 } else { |
|
668 # no IP line |
|
669 $msg_helo = $msg_ip; |
|
670 undef $msg_ip; |
|
671 } |
|
672 if (!($msg_helo =~ s/^HELO: //)) { |
|
673 # no HELO line |
|
674 $msg_env_from = $msg_helo; |
|
675 undef($msg_helo); |
|
676 } else { |
|
677 if (!($msg_env_from = <MSG>)) { |
|
678 close(MSG); |
|
679 return ($ise_msg, "message $msg truncated after HELO line"); |
|
680 } |
|
681 chop($msg_env_from); |
|
682 } |
|
683 if (!($msg_env_from =~ s/^env_From: //)) { |
|
684 # no env_from line |
|
685 $line = $msg_env_from; |
|
686 undef($msg_env_from); |
|
687 } else { |
|
688 $msg_mail_host = $msg_env_from; |
|
689 $msg_mail_host =~ s/.*mail_host=(.*)/$1/; |
|
690 $msg_env_from =~ s/<?([^\t> ]*).*/$1/; |
|
691 $line = <MSG>; |
|
692 } |
|
693 |
|
694 # Save the envelope env_To lines. |
|
695 for (;;) { |
|
696 if (!$line) { |
|
697 close(MSG); |
|
698 return ($ise_msg, "message $msg truncated in envelope"); |
|
699 } |
|
700 last if ($line =~ /^[\r\n]*$/); |
|
701 if ($line eq "abort\n") { |
|
702 close(MSG); |
|
703 return ("aborted transaction", ""); |
|
704 } |
|
705 push(@msg_env_to, $1) if ($line =~ /env_To:[\t ]*<?([^\t> ]+).*/); |
|
706 $line = <MSG>; |
|
707 } |
|
708 |
|
709 |
|
710 # Look for header lines that get checksums as we collect the whole message. |
|
711 $new_hdr = ""; |
|
712 undef($hdr_type); |
|
713 for (;;) { |
|
714 if (!($line = <MSG>)) { |
|
715 close(MSG); |
|
716 return ($ise_msg, "message $msg truncated in headers"); |
|
717 } |
|
718 |
|
719 # dccifd logs header lines with <CR><LF> but dccm uses <LF> |
|
720 $line =~ s/\r\n$/\n/; |
|
721 |
|
722 # deal with header continuation |
|
723 if ($line =~ /^[\t ]+/) { |
|
724 $new_hdr .= $line; |
|
725 $$cur_hdr .= $line if ($cur_hdr); |
|
726 next; |
|
727 } |
|
728 |
|
729 if ($cur_hdr) { |
|
730 # end a preceding interesting header |
|
731 $$cur_hdr =~ s/[\t ]*\n[\r\s]*/ /g; |
|
732 $$cur_hdr =~ s/^\s+//; |
|
733 $$cur_hdr =~ s/\s+$//; |
|
734 # emit a link |
|
735 if (!$no_body) { |
|
736 if ($hdr_type) { |
|
737 $msg_hdrs .= "$edit_link${url_ques}type=$hdr_type&val="; |
|
738 $msg_hdrs .= url_encode($$cur_hdr); |
|
739 $msg_hdrs .= "&msg=$msg&auto=1#cur_key\">"; |
|
740 chop($new_hdr); |
|
741 $msg_hdrs .= html_str_encode($new_hdr); |
|
742 $msg_hdrs .= "</A>\n"; |
|
743 undef($hdr_type); |
|
744 } else { |
|
745 $msg_hdrs .= html_str_encode($new_hdr); |
|
746 } |
|
747 } |
|
748 undef $cur_hdr; |
|
749 } else { |
|
750 # end preceding boring header |
|
751 $msg_hdrs .= html_str_encode($new_hdr); |
|
752 } |
|
753 |
|
754 # stop after the headers |
|
755 last if ($line eq "\n"); |
|
756 |
|
757 ++$num_hdrs; |
|
758 |
|
759 $new_hdr = $line; |
|
760 |
|
761 # Start an interesting header |
|
762 |
|
763 if ($line =~ s/^from:\s*//i) { |
|
764 $hdr_type = "from"; |
|
765 $msg_from = $line; |
|
766 $cur_hdr = \$msg_from; |
|
767 next; |
|
768 } |
|
769 if ($line =~ s/^(Message-ID):\s*//i) { |
|
770 $hdr_type = "Message-ID"; |
|
771 $misc_hdr = $line; |
|
772 $cur_hdr = \$misc_hdr; |
|
773 $seen_message_id = 1; |
|
774 next; |
|
775 } |
|
776 if ($line =~ s/^subject:\s*//i && 'subject:' =~ /^($sub_hdrs):/i) { |
|
777 $hdr_type = url_encode("substitute subject"); |
|
778 $msg_subject = $line; |
|
779 $cur_hdr = \$msg_subject; |
|
780 next; |
|
781 } |
|
782 |
|
783 if (!$no_body && $line =~ s/^($sub_hdrs):\s*//i) { |
|
784 $hdr_type = $1; |
|
785 $hdr_type =~ tr/A-Z/a-z/; |
|
786 $hdr_type = url_encode("substitute $hdr_type"); |
|
787 $misc_hdr = $line; |
|
788 $cur_hdr = \$misc_hdr; |
|
789 next; |
|
790 } |
|
791 } |
|
792 |
|
793 # fake empty Message-ID if required |
|
794 if (!$seen_message_id && $num_hdrs) { |
|
795 $msg_hdrs .= "$edit_link${url_ques}type="; |
|
796 $msg_hdrs .= "Message-ID"; |
|
797 $msg_hdrs .= "&val=%3c%3e&msg=$msg&auto=1#cur_key\">missing Message-ID</A>\n"; |
|
798 } |
|
799 |
|
800 # copy the body of the message |
|
801 for (;;) { |
|
802 if (!($line = <MSG>)) { |
|
803 close(MSG); |
|
804 return ($ise_msg, "message $msg truncated in body"); |
|
805 } |
|
806 last if ($line =~ $cksum_marker_p); |
|
807 $line =~ s/[ \t\r]+$//mg; |
|
808 $msg_body .= html_text_encode($line) if (!$no_body); |
|
809 } |
|
810 |
|
811 |
|
812 # copy the checksums |
|
813 while ($line = <MSG>) { |
|
814 # notice quoted checksums that are part of the body |
|
815 if ($line =~ $cksum_marker_p) { |
|
816 if (!$no_body) { |
|
817 $msg_body .= "<PRE class=mono>\n"; |
|
818 $msg_body .= $cksum_marker; |
|
819 $msg_body .= $msg_cksums; |
|
820 $msg_body .= "</PRE>\n"; |
|
821 } |
|
822 $msg_cksums = ''; |
|
823 $msg_result = ': '; |
|
824 next; |
|
825 } |
|
826 |
|
827 $msg_cksums .= $line; |
|
828 |
|
829 # Build a string of all of the reasons why the message should |
|
830 # have been accepted or rejected as we build the list of checksums. |
|
831 # Use italics for disabled checks. |
|
832 $msg_result .= "MTA " if ($line =~ /\bMTA-->spam(|\(first\))\b/); |
|
833 $msg_result .= "MTA-OK " if ($line =~ /\bMTA-->OK(|\(first\))\b/); |
|
834 $msg_result .= "BL " if ($line =~ /\bwlist-->spam\b/); |
|
835 $msg_result .= "WL " if ($line =~ /\bwlist-->OK\b/); |
|
836 $msg_result .= "DCC " if ($line =~ /\bDCC-->spam\b/); |
|
837 $msg_result .= "<I>DCC</I> " if ($line =~ /\bDCC-->spam\(off\)\b/); |
|
838 $msg_result .= "OK-DCC " if ($line =~ /\bDCC-->OK\b/); |
|
839 $msg_result .= "<I>OK-DCC</I> " if ($line =~ /\bDCC-->OK\(off\)\b/); |
|
840 $msg_result .= "Rep " if ($line =~ /\bRep-->spam\b/); |
|
841 $msg_result .= "<I>Rep</I> " if ($line =~ /\bRep-->spam\(off\)\b/); |
|
842 $msg_result .= "$1 " while ($line =~ s/\b(DNSBL\d?)-->spam\b//); |
|
843 $msg_result .= "<I>$1</I> " |
|
844 if ($line =~ s/\b(DNSBL\d?)-->spam\(off\)\b// ); |
|
845 |
|
846 # Prefix the string of reasons with what was done. |
|
847 if ($line =~ /^result: temporary greylist embargo/) { |
|
848 $msg_result = "Grey" . $msg_result; |
|
849 } elsif ($line =~ /^result: accept after greylist embargo/) { |
|
850 $msg_result = "OK-Grey" . $msg_result; |
|
851 } elsif ($line =~ /^result: accept/) { |
|
852 $msg_result = "OK" . $msg_result; |
|
853 } elsif ($line =~ /^result: reject temporarily/) { |
|
854 $msg_result = "Delay" . $msg_result; |
|
855 } elsif ($line =~ /^result: reject/) { |
|
856 $msg_result = "Reject" . $msg_result; |
|
857 } elsif ($line =~ /^result: discard/) { |
|
858 $msg_result = "Discard" . $msg_result; |
|
859 } elsif ($line =~ /^result: .*abort/) { |
|
860 $msg_result = "abort"; |
|
861 } |
|
862 } |
|
863 $msg_result =~ s/^: //; |
|
864 $msg_cksums = html_str_encode($msg_cksums) if (!$no_body); |
|
865 |
|
866 |
|
867 close(MSG); |
|
868 return undef; |
|
869 } |
|
870 |
|
871 |
|
872 |
|
873 sub decode_msg_name { |
|
874 my($str) = @_; |
|
875 my($val, $i, $c); |
|
876 |
|
877 use integer; |
|
878 |
|
879 $val = 0; |
|
880 for ($i = 0; $i < length($str); ++$i) { |
|
881 $c = ord(substr($str, $i, 1)); |
|
882 if ($c >= ord('a')) { |
|
883 $c = $c - ord('a') + 10; |
|
884 } elsif ($c >= ord('A')) { |
|
885 $c = $c - ord('A') + 10+26; |
|
886 } else { |
|
887 $c -= ord('0'); |
|
888 } |
|
889 $val = ($val * (10+26+26)) + $c; |
|
890 } |
|
891 return $val; |
|
892 } |
|
893 |
|
894 |
|
895 |
|
896 sub msg2path { |
|
897 my($msg, $path) = @_; |
|
898 |
|
899 $path = $logdir . '/' if (!defined $path); |
|
900 |
|
901 if (length($msg) >= 8) { |
|
902 $path .= sprintf("%03d/", decode_msg_name(substr($msg, 6, 2))); |
|
903 if (length($msg) >= 9) { |
|
904 $path .= sprintf("%02d/", decode_msg_name(substr($msg, 8, 1))); |
|
905 if (length($msg) >= 10) { |
|
906 $path .= sprintf("%02d/", decode_msg_name(substr($msg, 9, 1))); |
|
907 } |
|
908 } |
|
909 } |
|
910 |
|
911 return $path . 'msg.' . substr($msg, 0, 6); |
|
912 } |
|
913 |
|
914 |
|
915 |
|
916 # flush one cache file |
|
917 sub cache_write_file { |
|
918 my($buf, $cnum) = @_; |
|
919 my($tmp, $cfname, $date); |
|
920 |
|
921 $tmp = "msg.cache." . "new." . $$; |
|
922 if (!sysopen(CFILE, $tmp, O_WRONLY | O_CREAT, 0660)){ |
|
923 print STDERR "open($tmp): $!\n"; |
|
924 return undef; |
|
925 } |
|
926 if (syswrite(CFILE, $cache_version) != length($cache_version) |
|
927 || syswrite(CFILE, $buf) != length($buf)) { |
|
928 print STDERR "syswrite $tmp: $!\n"; |
|
929 close(CFILE); |
|
930 unlink($tmp); |
|
931 return undef; |
|
932 } |
|
933 |
|
934 close(CFILE); |
|
935 $cnum =~ /(\d+)/; $cnum = $1; # suppress Perl taint warning |
|
936 $date = $cnum * (24*3600); |
|
937 if ($date <= time) { |
|
938 utime($date, $date, $tmp) |
|
939 || print STDERR "utime($date, $date, $tmp): $!\n"; |
|
940 } |
|
941 $cfname = "msg.cache." . $cnum; |
|
942 if (!rename($tmp, $cfname)) { |
|
943 print STDERR "rename($tmp, $cfname): $!\n"; |
|
944 unlink($tmp); |
|
945 return undef; |
|
946 } |
|
947 |
|
948 $msgs_cache_state{$cnum} = 1; |
|
949 return 1; |
|
950 } |
|
951 |
|
952 |
|
953 |
|
954 # flush the cache files |
|
955 sub cache_flush { |
|
956 my($cache_files, $log_files, |
|
957 $cnum, $cfname, $state, $new_cnum, $msg, $buf, $buf_start); |
|
958 |
|
959 if (! -w ".") { |
|
960 my $marker = "$logout_tmpdir/msg.$user-nocache"; |
|
961 if (! -f $marker |
|
962 || (stat(_))[9] < time()-(4*3600)) { |
|
963 if (!open(CFILE,">>",$marker)){ |
|
964 print STDERR "open($marker): $!\n"; |
|
965 } else { |
|
966 print CFILE "$logdir not writable for cache files\n"; |
|
967 close CFILE; |
|
968 } |
|
969 print STDERR "$logdir not writable for cache files\n"; |
|
970 } |
|
971 return; |
|
972 } |
|
973 |
|
974 $cache_files = 0; |
|
975 $log_files = 0; |
|
976 $buf_start = 0; |
|
977 |
|
978 $cnum = 0; |
|
979 foreach $msg (@msgs_num) { |
|
980 # one cache file per day, so pick the cache for this log file |
|
981 # note that the files are sorted by mtime |
|
982 $new_cnum = $msgs_cache{$msg}[0] / (24*3600); |
|
983 |
|
984 # skip this log file if its cache file is good |
|
985 next if ($msgs_cache_state{$new_cnum}); |
|
986 |
|
987 # close the current cache file if we are dealing with a new day |
|
988 # and so a new cache file |
|
989 if ($cnum != $new_cnum) { |
|
990 if ($log_files - $buf_start > 10) { |
|
991 ++$cache_files; |
|
992 return if (!cache_write_file($buf, $cnum)); |
|
993 $buf_start = $log_files; |
|
994 } else { |
|
995 # forget the cache file if it would be tiny |
|
996 $log_files= $buf_start; |
|
997 } |
|
998 undef $buf; |
|
999 $cnum = $new_cnum; |
|
1000 } |
|
1001 |
|
1002 $buf .= pack($cache_pack, |
|
1003 $msgs_cache{$msg}[0], |
|
1004 $msgs_cache{$msg}[1], |
|
1005 $msg); |
|
1006 ++$log_files; |
|
1007 } |
|
1008 if ($log_files - $buf_start > 10) { |
|
1009 ++$cache_files; |
|
1010 cache_write_file($buf, $cnum); |
|
1011 } else { |
|
1012 $log_files= $buf_start; |
|
1013 } |
|
1014 |
|
1015 # delete junk cache files |
|
1016 while (($cnum, $state) = each %msgs_cache_state) { |
|
1017 next if ($state); |
|
1018 $cfname = "msg.cache." . $cnum; |
|
1019 if (-f $cfname && !unlink($cfname)) { |
|
1020 print STDERR "unlink($cfname): $!\n"; |
|
1021 return; |
|
1022 } |
|
1023 } |
|
1024 |
|
1025 debug_time("flushed $cache_files cache files with $log_files files"); |
|
1026 } |
|
1027 |
|
1028 |
|
1029 |
|
1030 # get the list of messages |
|
1031 # The first arg is the current file |
|
1032 # Try to limit the size of the table to the second arg |
|
1033 # divide days worth of files to fit the page size if it is <0 |
|
1034 # |
|
1035 # sets globals %msgs_date, %msgs_result, %msgs_from, %msgs_subject, |
|
1036 # $msg_day_first, $msg_day_last, $msg_first, $msg_last, |
|
1037 # $msg_newer, $msg_part_num, @msgs_num |
|
1038 sub get_log_msgs { |
|
1039 my($page_msg, # target log message |
|
1040 $page_size, # log files / web page |
|
1041 $mode # 0=old, 1=reverse sort & divide days |
|
1042 ) = @_; |
|
1043 my($cache_len, $need_flush, $cache_parse_limit, $dir_len, |
|
1044 $line, $msg, $entry, $days, $sort_order, |
|
1045 $msg_tgt, $date_tgt, $date_cur, $date1, $msg_num, $msg_num_prev, $start); |
|
1046 |
|
1047 $cache_parse_limit = 100; |
|
1048 |
|
1049 $cache_version = "DCC msg.cache version 3\n"; |
|
1050 $cache_pack = "LLA10"; |
|
1051 $cache_line_len = length(pack($cache_pack, 0)); |
|
1052 $msg_encode_str = ("0123456789" |
|
1053 . "abcdefghijklmnopqrstuvwxyz" |
|
1054 . "ABCDEFGHIJKLMNOPQRSTUVWXYZ"); |
|
1055 |
|
1056 # reverse the sort for old callers |
|
1057 $sort_order = !$mode ? -1 : 1; |
|
1058 |
|
1059 |
|
1060 # Build a list of log file names and dates |
|
1061 # Use cache files of names and dates. Validate the cache |
|
1062 # files by checking i-numbers. Use the `ls` command because the |
|
1063 # the Perl readdir() function does not provide d_ino/d_fileno. |
|
1064 |
|
1065 chdir($logdir) || html_whine("chdir($logdir): $!"); |
|
1066 |
|
1067 # ls -ifC1 would be faster, but it does not work on files on Solaris |
|
1068 html_whine("ls -iC1 $logdir: $!") |
|
1069 if (!open(DIR, "find . -name 'msg.*' | @DCC_XARGS@ /bin/ls -iC1 |")); |
|
1070 $dir_len = 0; |
|
1071 while ($line = <DIR>) { |
|
1072 # find simple log files as well as log files in DDD, DDD/HH, and |
|
1073 # DDD/HH/MM subdirectories |
|
1074 if ($line =~ /^\s*(\d+)\s+\.\/((\d\d\d)(\/(\d\d))?(\/(\d\d))?\/)?msg\.([A-Za-z\d]{6})\s*$/) { |
|
1075 my ($filename, $subdir); |
|
1076 $filename = $8; |
|
1077 if (defined $3) { |
|
1078 # encode the subdirectory |
|
1079 next if ($3 > 366); |
|
1080 use integer; |
|
1081 $filename .= (substr($msg_encode_str, |
|
1082 $3 / length($msg_encode_str), 1) |
|
1083 . substr($msg_encode_str, |
|
1084 $3 % length($msg_encode_str), 1)); |
|
1085 if (defined $5) { |
|
1086 next if ($5 >= 24); |
|
1087 $filename .= substr($msg_encode_str, $5, 1); |
|
1088 if (defined $7) { |
|
1089 next if ($7 >= 60); |
|
1090 $filename .= substr($msg_encode_str, $7, 1); |
|
1091 } |
|
1092 } |
|
1093 } |
|
1094 $msgs_cache{$filename}[1] = $1; |
|
1095 ++$dir_len; |
|
1096 next; |
|
1097 } |
|
1098 # notice cache files |
|
1099 if ($line =~ /^\s*\d+\s+\.\/(msg\.cache\.(\d{5}))\s*$/ |
|
1100 && -f $1 |
|
1101 && (((stat(_))[7] - length($cache_version)) |
|
1102 % $cache_line_len) == 0) { |
|
1103 $msgs_cache_state{$2} = 0; |
|
1104 next; |
|
1105 } |
|
1106 } |
|
1107 close(DIR); |
|
1108 debug_time("$dir_len files found"); |
|
1109 |
|
1110 # load the cache files |
|
1111 # Because cache files are named with dates, are read in sorted |
|
1112 # order, and have sorted contents, we do not need to sort |
|
1113 # what we read from them. |
|
1114 $cache_len = 0; |
|
1115 $need_flush = 0; |
|
1116 foreach my $cnum (sort keys(%msgs_cache_state)) { |
|
1117 my($total, $good, $date_lo, $date_hi); |
|
1118 |
|
1119 next if (!open(CFILE, "msg.cache." . $cnum)); |
|
1120 |
|
1121 if (!read(CFILE, $buf, length($cache_version)) |
|
1122 || $buf ne $cache_version) { |
|
1123 close CFILE; |
|
1124 next; |
|
1125 } |
|
1126 |
|
1127 # the names in a cache file are for a single day |
|
1128 $date_lo = $cnum * 24*3600; |
|
1129 $date_hi = $date_lo + 24*3600 - 1; |
|
1130 $good = $total = 0; |
|
1131 while (read(CFILE, $buf, $cache_line_len)) { |
|
1132 my($date, $ino, $msg) = unpack($cache_pack, $buf); |
|
1133 ++$total; |
|
1134 |
|
1135 # a cache file is bogus if it contains bad dates |
|
1136 last if ($date < $date_lo || $date > $date_hi); |
|
1137 |
|
1138 # skip deleted log files |
|
1139 next if (!exists($msgs_cache{$msg})); |
|
1140 |
|
1141 # skip cached file names that have been recycled |
|
1142 next if ($msgs_cache{$msg}[1] != $ino); |
|
1143 |
|
1144 $msgs_cache{$msg}[0] = $date; |
|
1145 push @msgs_num, $msg; |
|
1146 |
|
1147 ++$good; |
|
1148 } |
|
1149 close(CFILE); |
|
1150 if ($good == 0 || $good+20 < $total) { |
|
1151 $need_flush = 1; |
|
1152 } elsif ($good == $total) { |
|
1153 $msgs_cache_state{$cnum} = 1; |
|
1154 } |
|
1155 $cache_len += $good; |
|
1156 } |
|
1157 debug_time("$cache_len files cached"); |
|
1158 |
|
1159 # If there are any new log files, |
|
1160 # then we must get their dates and then sort all of the names |
|
1161 if ($cache_len != $dir_len) { |
|
1162 $need_flush = 1 if ($dir_len > $cache_len+100); |
|
1163 $msg_num = 0; |
|
1164 while (($msg, $entry) = each %msgs_cache) { |
|
1165 next if (@$entry[0]); # we know about this file from cache |
|
1166 |
|
1167 my $date = (stat msg2path($msg))[9]; |
|
1168 if (!$date) { |
|
1169 # forget this file if we cannot stat() it |
|
1170 delete $msgs_cache{$msg}; |
|
1171 next; |
|
1172 } |
|
1173 @$entry[0] = $date; |
|
1174 $msgs_cache_state{$date / (24*3600)} = 0; |
|
1175 ++$msg_num; |
|
1176 } |
|
1177 debug_time("$msg_num files dated"); |
|
1178 |
|
1179 # this is obscure but much faster than using comparison functions that |
|
1180 # do hash lookups for each comparison of the sort |
|
1181 @msgs_num = map {my @a = unpack("NA10",$_); $a[1]} |
|
1182 (sort map pack("NA10", |
|
1183 $msgs_cache{$_}[0]*$sort_order, |
|
1184 $_), |
|
1185 keys %msgs_cache); |
|
1186 debug_time("sorted " . ($#msgs_num+1) . " files"); |
|
1187 } |
|
1188 |
|
1189 # find the target message that must be listed |
|
1190 $msg_tgt = ($sort_order > 0 && $#msgs_num >= 0) ? $#msgs_num : 0; |
|
1191 if ($page_msg) { |
|
1192 for ($msg_num = 0; $msg_num <= $#msgs_num; ++$msg_num) { |
|
1193 if ($msgs_num[$msg_num] eq $page_msg) { |
|
1194 $msg_tgt = $msg_num; |
|
1195 last; |
|
1196 } |
|
1197 } |
|
1198 debug_time("found #" . $msg_tgt); |
|
1199 } |
|
1200 |
|
1201 # we are finished if the caller only wanted the list of files |
|
1202 # perhaps for URLs pointing to previous and next files |
|
1203 if (!$page_size || $page_size < 1 || $#msgs_num < 0) { |
|
1204 cache_flush() if ($need_flush); |
|
1205 $msg_first = $msg_tgt; |
|
1206 $msg_last = $#msgs_num; |
|
1207 $msg_newer = $#msgs_num; |
|
1208 $msg_part_num = 0; |
|
1209 $msgs_mtime{$page_msg} = 1 if (!$mode && $page_msg); |
|
1210 return; |
|
1211 } |
|
1212 |
|
1213 # Get summary information from all of the files on the target day, the |
|
1214 # last file on the previous day, and on the first file on the next day. |
|
1215 # |
|
1216 # walk backward from the target to the first log file of the target day |
|
1217 $date_tgt = $date_cur = (localtime $msgs_cache{$msgs_num[$msg_tgt]}[0])[7]; |
|
1218 for ($msg_day_first = $msg_tgt; |
|
1219 $msg_day_first > 0; |
|
1220 $msg_day_first = $msg_num) { |
|
1221 $msg_num = $msg_day_first-1; |
|
1222 $date_cur = (localtime $msgs_cache{$msgs_num[$msg_num]}[0])[7]; |
|
1223 last if ($date_cur != $date_tgt); |
|
1224 } |
|
1225 if (!$mode) { |
|
1226 $msg_part_num = 0; |
|
1227 $msg_first = $msg_day_first; |
|
1228 } else { |
|
1229 $msg_part_num = ($msg_tgt - $msg_day_first) / $page_size; |
|
1230 $msg_first = $msg_day_first + ($msg_part_num * $page_size); |
|
1231 } |
|
1232 |
|
1233 # walk forward to the end of the day or $page_size files |
|
1234 $days = 0; # count space used by date headings |
|
1235 $msg_last = $msg_first + $page_size-1; |
|
1236 $msg_last = $#msgs_num if ($msg_last > $#msgs_num); |
|
1237 $msg_newer = $msg_first + $page_size; |
|
1238 $msg_newer = $#msgs_num if ($msg_newer > $#msgs_num); |
|
1239 $msg_day_last = $#msgs_num; |
|
1240 $date1 = $date_tgt; |
|
1241 for ($msg_num = $msg_tgt+1; $msg_num <= $#msgs_num; ++$msg_num) { |
|
1242 $date_cur = (localtime $msgs_cache{$msgs_num[$msg_num]}[0])[7]; |
|
1243 next if ($date_cur == $date1); |
|
1244 |
|
1245 ++$days; |
|
1246 |
|
1247 if ($date1 == $date_tgt) { |
|
1248 $msg_day_last = $msg_num-1; |
|
1249 |
|
1250 # the "newer" link goes to the first file of the next day if |
|
1251 # the current day fits on the web page |
|
1252 $msg_newer = $msg_num if (!$msg_newer || $msg_num < $msg_newer); |
|
1253 } |
|
1254 |
|
1255 if ($msg_num > $msg_first + $page_size - $days) { |
|
1256 $msg_last = $msg_num-1 if (!$mode); |
|
1257 last; |
|
1258 } |
|
1259 |
|
1260 $msg_last = $msg_num-1 if ($#msgs_num > $msg_first+$page_size-$days); |
|
1261 $date1 = $date_cur; |
|
1262 } |
|
1263 |
|
1264 if ($mode) { |
|
1265 ++$msg_part_num if ($msg_part_num != 0 |
|
1266 || $msg_first + $page_size <= $msg_day_last); |
|
1267 # overlap the parts of a day by a line |
|
1268 ++$msg_last if ($msg_part_num != 0 && $msg_last < $msg_day_last); |
|
1269 } |
|
1270 |
|
1271 # parse the log files to get the data |
|
1272 for ($msg_num = $msg_first; $msg_num <= $msg_last; ++$msg_num) { |
|
1273 $msg = $msgs_num[$msg_num]; |
|
1274 my(@error) = parse_log_msg($msg, "no body"); |
|
1275 if (defined $error[0]) { |
|
1276 $msgs_date{$msg} = strftime("%x %X", |
|
1277 localtime($msgs_cache{$msg}[0])) |
|
1278 if (!$msgs_date{$msg} && $msgs_cache{$msg}[0]); |
|
1279 $msgs_from{$msg} = "<STRONG class=warn>$error[0]</STRONG>"; |
|
1280 $msgs_result{$msg} = ''; |
|
1281 $msgs_subject{$msg} = "<STRONG class=warn>$error[1]</STRONG>"; |
|
1282 } else { |
|
1283 $msgs_date{$msg} = $msg_date; |
|
1284 $msgs_from{$msg} = hdr_trim_encode($msg_from |
|
1285 ? $msg_from |
|
1286 : $msg_env_from); |
|
1287 $msgs_result{$msg} = $msg_result ? $msg_result : " "; |
|
1288 $msgs_subject{$msg} = hdr_trim_encode($msg_subject); |
|
1289 } |
|
1290 } |
|
1291 debug_time(($msg_last - $msg_first + 1) . " log files parsed"); |
|
1292 |
|
1293 cache_flush() if ($need_flush); |
|
1294 } |
|
1295 |
|
1296 |
|
1297 |
|
1298 ########################################################################## |
|
1299 # whiteclnt file functions |
|
1300 |
|
1301 # The file is represented as an array or list of references to 3-tuples. |
|
1302 # The first of the three is the whitelist entry in a canonical form |
|
1303 # as a key uniquely identifying the entry. |
|
1304 # The second is a comment string of zero or more comment lines. |
|
1305 # The third is the DCC whiteclnt entry. |
|
1306 # |
|
1307 # The canonical form and the whiteclnt line of the first 3-tuple for a file |
|
1308 # are null, because it contains the comments, if any, before the file's |
|
1309 # preamble of dans when the file has been changed and flags. |
|
1310 # The file[1] is an empty slot for adding option settings. |
|
1311 # The last triple in a file may also lack a whitelist entry. |
|
1312 |
|
1313 # There is a hash or dictionary of references to entries in the list |
|
1314 |
|
1315 |
|
1316 # lock, read, and parse the file |
|
1317 sub read_whiteclnt { |
|
1318 my($file_ref, $dict_ref) = @_; |
|
1319 my($entry, $prev_entry, $comment); |
|
1320 |
|
1321 @$file_ref = (); |
|
1322 %$dict_ref = (); |
|
1323 |
|
1324 # Creating the file here is usually a waste of effort, because |
|
1325 # it must be writable by both the HTTP server and dccm or dccifd. |
|
1326 # They are probably not in any common group. |
|
1327 # Let the @libexecdir@/newwebuser script create the per-user |
|
1328 # directories and files. |
|
1329 # Because whitelists might be a little sensitive, they should not be |
|
1330 # readable by "other" |
|
1331 html_whine("open($whiteclnt): $!") |
|
1332 if (!sysopen(WHITECLNT, $whiteclnt, O_RDWR | O_CREAT, 0660)); |
|
1333 chmod(0660, $whiteclnt); |
|
1334 |
|
1335 html_whine("flock($whiteclnt): $!") |
|
1336 if (!flock(WHITECLNT, LOCK_EX | LOCK_NB)); |
|
1337 |
|
1338 $comment = ""; |
|
1339 while ($entry = <WHITECLNT>) { |
|
1340 # end the last line properly even if the file doesn't |
|
1341 $entry .= "\n" if (substr($entry,-1) ne "\n"); |
|
1342 |
|
1343 # collect lines until we get a non-comment |
|
1344 if ($entry =~ /^\s*(#|$)/) { |
|
1345 $comment .= $entry; |
|
1346 next; |
|
1347 } |
|
1348 |
|
1349 # use the previous count if the current value is missing, |
|
1350 # because that is what dcclib/parse_whitefile.c does. |
|
1351 $entry = "$1$entry" |
|
1352 if ($entry =~ /^[ \t]/ |
|
1353 && $#$file_ref > 0 |
|
1354 && ($prev_entry = ${${$file_ref}[$#$file_ref]}[2]) |
|
1355 && $prev_entry =~ /^(\S+)/); |
|
1356 |
|
1357 add_white_entry($file_ref, $dict_ref, $comment, $entry); |
|
1358 $comment = ""; |
|
1359 } |
|
1360 |
|
1361 # save a non-trivial trailing comment |
|
1362 add_white_entry($file_ref, $dict_ref, $comment, "") |
|
1363 if ($comment && $comment !~ /^\s*$/); |
|
1364 } |
|
1365 |
|
1366 |
|
1367 |
|
1368 # read the main whiteclnt file to determine the default option settings |
|
1369 sub read_whitedefs { |
|
1370 my($def_ref) = @_; |
|
1371 my(@sb1, @sb2, $line, @parsed, $bydef); |
|
1372 |
|
1373 |
|
1374 # these defaults for the defaults must match dcclib/parse_whitefile.c |
|
1375 # or elsewhere in the DCC client source (e.g. for discardok) |
|
1376 %$def_ref = (); |
|
1377 $bydef = " <SMALL>by default</SMALL>"; |
|
1378 ${$def_ref}{dccenable} = "<STRONG>on</STRONG>$bydef"; |
|
1379 ${$def_ref}{greyfilter} = "<STRONG>on</STRONG>$bydef"; |
|
1380 ${$def_ref}{greylog} = "<STRONG>on</STRONG>$bydef"; |
|
1381 ${$def_ref}{mtafirst} = "<STRONG>last</STRONG>$bydef"; |
|
1382 ${$def_ref}{rep} = "<STRONG>off</STRONG>$bydef"; |
|
1383 ${$def_ref}{dnsbl1} = "<STRONG>off</STRONG>$bydef"; |
|
1384 ${$def_ref}{dnsbl2} = "<STRONG>off</STRONG>$bydef"; |
|
1385 ${$def_ref}{dnsbl3} = "<STRONG>off</STRONG>$bydef"; |
|
1386 ${$def_ref}{logall} = "<STRONG>off</STRONG>$bydef"; |
|
1387 ${$def_ref}{discardok} = "<STRONG>delay mail</STRONG>$bydef"; |
|
1388 |
|
1389 foreach my $ck (split(/,/,$thold_cks)) { |
|
1390 my $nm = "thold-$ck"; |
|
1391 if (!$conf_cks_tholds{$ck}) { |
|
1392 ${$def_ref}{$nm} = "<STRONG>Never</STRONG>$bydef"; |
|
1393 } else { |
|
1394 ${$def_ref}{$nm} = $conf_cks_tholds{$ck}; |
|
1395 } |
|
1396 } |
|
1397 |
|
1398 if (!sysopen(MAINWHITE, $main_whiteclnt, O_RDONLY, 0)) { |
|
1399 print STDERR "open(${main_whiteclnt}: $!\n"; |
|
1400 return; |
|
1401 } |
|
1402 |
|
1403 if (!(@sb1 = stat(MAINWHITE))) { |
|
1404 print STDERR "stat(${main_whiteclnt}: $!\n"; |
|
1405 } elsif (!(@sb2 = stat(WHITECLNT))) { |
|
1406 print STDERR "stat(${$whiteclnt}: $!\n"; |
|
1407 } elsif ($sb1[0] == $sb2[0] && $sb1[1] == $sb2[1]) { |
|
1408 # ignore it if we are somehow working on the main file |
|
1409 } else { |
|
1410 while ($line = <MAINWHITE>) { |
|
1411 # skip everything except option settings |
|
1412 next if ($line !~ /^\s*option\s+/i); |
|
1413 |
|
1414 @parsed = parse_white_entry($line, "option"); |
|
1415 next if (!$parsed[1]); |
|
1416 ${$def_ref}{$parsed[0]} = "<STRONG>$parsed[2]</STRONG> <SMALL>by default in $main_whiteclnt</SMALL>"; |
|
1417 } |
|
1418 } |
|
1419 close(MAINWHITE); |
|
1420 } |
|
1421 |
|
1422 |
|
1423 |
|
1424 # add an entry to our image of the file |
|
1425 # sets the globals: |
|
1426 # $whiteclnt_version, #webuser version ... |
|
1427 # $whiteclnt_notify, #webuser mail-notify=X mailbox=Y |
|
1428 # $whiteclnt_notify_pat, #regex for #webuser mail-notify=X mailbox=Y |
|
1429 # $whiteclnt_lock, #webuser (un)locked |
|
1430 # $whiteclnt_cur_key #editing position in the file |
|
1431 # $whiteclnt_change_log, #list of dates when file was changed |
|
1432 |
|
1433 sub add_white_entry { |
|
1434 my($file_ref, $dict_ref, $comment, $line) = @_; |
|
1435 my(@parsed); |
|
1436 |
|
1437 # trim unneeded white space |
|
1438 $line =~ s/\s+$//; |
|
1439 $comment =~ s/[ \t]+$//mg; |
|
1440 |
|
1441 # deal with the preamble. |
|
1442 # The preamble consists of the comments that start the file. |
|
1443 if (! @$file_ref) { |
|
1444 my($preamble, @buf); |
|
1445 |
|
1446 # remove the change-history, version, and parameters from the preamble |
|
1447 $whiteclnt_version = "#webuser version 1.0\n"; |
|
1448 while ($comment =~ s/^#webuser version ([0-9.]+)\n/ \n/m) { |
|
1449 # for now, insist on version 1.0 |
|
1450 html_whine("unrecognized version $1 in $whiteclnt") |
|
1451 if ($1 ne "1.0"); |
|
1452 } |
|
1453 |
|
1454 $whiteclnt_notify_pat = '(#webuser mail-notify=)(on|off)( mailbox=)([-_a-z0-9]*)'; |
|
1455 $whiteclnt_notify = "#webuser mail-notify=off mailbox=\n"; |
|
1456 while ($comment =~ s/^$whiteclnt_notify_pat\n/ \n/im) { |
|
1457 $whiteclnt_notify = "$1$2$3$4\n"; |
|
1458 } |
|
1459 |
|
1460 $whiteclnt_lock = "#webuser unlocked\n"; |
|
1461 while ($comment =~ s/^#\s*webuser\s+unlocked\n/ \n/im) { |
|
1462 } |
|
1463 while ($comment =~ s/^#\s*webuser\s+locked\n/ \n/im) { |
|
1464 $whiteclnt_lock = "#webuser locked\n"; |
|
1465 } |
|
1466 |
|
1467 $whiteclnt_cur_key = ""; |
|
1468 while ($comment =~ s/^#\s*webuser\s+cur_key\s+(.*)\n/ \n/im) { |
|
1469 $whiteclnt_cur_key = $1; |
|
1470 } |
|
1471 |
|
1472 $whiteclnt_change_log = ""; |
|
1473 while ($comment =~ s/^#\s*webuser created\s+(.+\n)/ \n/im) { |
|
1474 $whiteclnt_change_log = "#webuser created $1"; |
|
1475 } |
|
1476 undef(@buf); |
|
1477 while ($comment =~ s/^#webuser\s+changed\s+(.+\n)/ \n/im) { |
|
1478 push(@buf, "#webuser changed $1"); |
|
1479 } |
|
1480 # keep only the last 20 dates of change |
|
1481 if (@buf) { |
|
1482 my($start); |
|
1483 $start = $#buf-20; |
|
1484 $start = 0 |
|
1485 if ($start < 0); |
|
1486 $whiteclnt_change_log .= join('', @buf[$start .. $#buf]); |
|
1487 } |
|
1488 |
|
1489 # We have removed the parameter lines from the first comment of the |
|
1490 # file and replaced them with " \n" |
|
1491 # Before starting we remove blanks from the ends of lines. |
|
1492 # The first block of comments must now be divided between (1) comments |
|
1493 # about the file and (2) comments about the first real line of the file. |
|
1494 if ($comment =~ s/^(.* \n)//s) { |
|
1495 # Take the comment lines through the last marker if there were any. |
|
1496 # Add the first blank line after the markers if present. |
|
1497 $preamble = $1; |
|
1498 $preamble .= "\n" if ($comment =~ s/^\n//); |
|
1499 # remove the markers for detected parameter lines |
|
1500 $preamble =~ s/ \n//g; |
|
1501 } else { |
|
1502 # without parameters, take lines through the first blank line as (1) |
|
1503 $preamble = ($comment =~ s/(.*?\n\n)//s |
|
1504 || $comment =~ s/(.*?\n[# \t]+\n)//s) ? $1 : ""; |
|
1505 } |
|
1506 |
|
1507 # start the memory copy of the file with the preamble |
|
1508 # and the spare slot for option changes |
|
1509 @$file_ref = ([undef, $preamble, ""], ["", undef, undef]); |
|
1510 |
|
1511 # finished if the file has no entries, |
|
1512 return if (!$line && !$comment); |
|
1513 |
|
1514 # or deal with the first entry |
|
1515 } |
|
1516 |
|
1517 # If the line makes sense, remember where it will be. |
|
1518 # Treat the line as a comment if it makes no sense |
|
1519 @parsed = parse_white_entry($line, ""); |
|
1520 if (!$parsed[1]) { |
|
1521 $comment .= $line; |
|
1522 $comment .= "\n" if ($comment !~ /\n$/); |
|
1523 push @$file_ref, [undef, $comment, ""]; |
|
1524 } else { |
|
1525 my($cur_key, $entry, $i, $k); |
|
1526 |
|
1527 $cur_key = $parsed[0]; |
|
1528 $entry = [$cur_key, $comment, $parsed[1]]; |
|
1529 push @$file_ref, $entry; |
|
1530 |
|
1531 if (${$dict_ref}{$cur_key}) { |
|
1532 $i = 0; |
|
1533 # mark duplicate values for eventual deletion |
|
1534 # keep the last setting in the file |
|
1535 while (${$dict_ref}{$k = "DUP-$i-$cur_key"}) { |
|
1536 ++$i; |
|
1537 } |
|
1538 ${$dict_ref}{$k} = ${$dict_ref}{$cur_key}; |
|
1539 } |
|
1540 ${$dict_ref}{$cur_key} = $entry; |
|
1541 } |
|
1542 } |
|
1543 |
|
1544 |
|
1545 # check the syntax of IP addresses and CIDR blocks |
|
1546 # return undef if ok but an error string if not |
|
1547 sub check_ip { |
|
1548 my($value) = @_; |
|
1549 |
|
1550 return "blank or missing IP address" if (!$value); |
|
1551 |
|
1552 my $addr = $value; |
|
1553 if ($addr =~ s/(.*)\/(\d+)/$1/) { |
|
1554 my $cidr = $2; |
|
1555 $cidr += 96 if ($addr =~ /:/); |
|
1556 return "\"$value\" is not a valid CIDR block" |
|
1557 if ($cidr > 128 || $cidr <= 0); |
|
1558 } |
|
1559 |
|
1560 # it would be better to use some library to parse the IP address, |
|
1561 # but in 2005, there was no Perl module that could handle IPv6 |
|
1562 # addresses and is almost always available |
|
1563 return "\"$value\" is not a valid IP address" |
|
1564 if ($addr !~ /^[.:0-9a-f]+$/); |
|
1565 |
|
1566 if ($addr =~ /:/) { |
|
1567 # IPv6 |
|
1568 my $colons = $addr; |
|
1569 $colons =~ s/[^:]+//g; |
|
1570 $colons = length($colons); |
|
1571 if ($addr =~ /^::/ && $colons <= 7) { |
|
1572 ++$colons while ($colons < 7 && $addr =~ s/^::/::0:/); |
|
1573 $addr =~ s/^::/0:/ if ($colons == 7); |
|
1574 } elsif ($addr =~ /::$/ && $colons <= 7) { |
|
1575 ++$colons while ($colons < 7 && $addr =~ s/::$/:0::/); |
|
1576 $addr =~ s/::$/:0/ if ($colons == 7); |
|
1577 } else { |
|
1578 ++$colons while ($colons < 7 && $addr =~ s/::/::0:/); |
|
1579 $addr =~ s/::/:0:/ if ($colons == 7); |
|
1580 } |
|
1581 return "$value is not a valid IP address" if ($colons > 7); |
|
1582 |
|
1583 $addr =~ s/^([0-9a-f]{1,4}:)+//; |
|
1584 $addr =~ s/^[0-9a-f]{1,4}$/127.0.0.1/; |
|
1585 return "\"$value\" is not a valid IP address" if ($addr =~ /:/); |
|
1586 |
|
1587 # stop looking at IPv6 address with either the IPv4 trailing part |
|
1588 #or a fake 127.0.0.1 |
|
1589 } |
|
1590 |
|
1591 my $quads = 0; |
|
1592 while ($addr =~ s/^(\d{1,3})\.//) { |
|
1593 return "\"$value\" is not a valid IP address" if ($1 > 255); |
|
1594 ++$quads; |
|
1595 } |
|
1596 return "$value is not a valid IP address" if ($addr > 255 || $quads > 3); |
|
1597 |
|
1598 # we should now check for collisions among addresses |
|
1599 |
|
1600 return undef; |
|
1601 } |
|
1602 |
|
1603 |
|
1604 |
|
1605 sub check_present { |
|
1606 my($type, $value) = @_; |
|
1607 |
|
1608 # dcc_str2ck() via dcc_parse_ck() ignores outside quotes and <>, |
|
1609 # whitespace, and upper/lower case, and trailing periods. So our key must |
|
1610 # also. |
|
1611 # The value for the line in the file need not be as clean. |
|
1612 $value =~ s/^\s+//; |
|
1613 $value =~ s/\s+$//; |
|
1614 $value =~ s/^<\s*(.+)\s*>$/$1/ |
|
1615 if ($value !~ s/^"\s*(.+)\s*"$/$1/); |
|
1616 $value =~ s/\.+$//; |
|
1617 |
|
1618 return ($type, $value) if ($value); |
|
1619 return ($type, "<>", "blank or missing $type value"); |
|
1620 } |
|
1621 |
|
1622 |
|
1623 |
|
1624 sub check_hex { |
|
1625 my($type, $value) = @_; |
|
1626 |
|
1627 return ($type, $value, "blank or missing $type value") |
|
1628 if (!$value); |
|
1629 |
|
1630 return ($type, $value) if ($value =~ s/([0-9a-f]{8})\s+([0-9a-f]{8}) |
|
1631 \s+([0-9a-f]{8})\s+([0-9a-f]{8})$ |
|
1632 /$1 $2 $3 $4/ix); |
|
1633 |
|
1634 return ($type, $value, "\"$value\" is an invalid $type checksum"); |
|
1635 } |
|
1636 |
|
1637 |
|
1638 |
|
1639 # canonicalize a whitelist checksum "type value" string |
|
1640 # return a (type, value) pair or (x, x, "error string") triple |
|
1641 sub parse_type_value { |
|
1642 my $value = $_[0]; |
|
1643 |
|
1644 # Check for type |
|
1645 # Don't support received checksums. |
|
1646 # Body checksums must be hex. |
|
1647 $value =~ s/\s+$//; |
|
1648 |
|
1649 return ("IP", $value, check_ip($value)) |
|
1650 if ($value =~ s/^IP:?(\s*|$)//i); |
|
1651 |
|
1652 return check_present("env_From", $value) |
|
1653 if ($value =~ s/^env[-_]from:?(\s+|$)//i); |
|
1654 |
|
1655 return check_present("env_To", $value) |
|
1656 if ($value =~ s/^env[-_]To:?(\s+|$)//i); |
|
1657 |
|
1658 return check_present("From", $value) |
|
1659 if ($value =~ s/^from:?(\s+|$)//i); |
|
1660 |
|
1661 return check_present("Message-ID", $value) |
|
1662 if ($value =~ s/^message[-_]id:?(\s+|$)//i); |
|
1663 |
|
1664 # don't worry much about substitute types. |
|
1665 return check_present("substitute $1", $value) |
|
1666 if ($value =~ s/^substitute\s+([-a-z_0-9]+)+:?(\s+|$)//i); |
|
1667 |
|
1668 return check_hex("hex Body", $value) |
|
1669 if ($value =~ s/^hex\s+body:?(\s+|$)//i); |
|
1670 |
|
1671 return check_hex("hex Fuz$1", $value) |
|
1672 if ($value =~ s/^hex\s+fuz([12]):?(\s+|$)//i); |
|
1673 |
|
1674 return (undef, undef, "unrecognized whiteclnt value \"$value\""); |
|
1675 } |
|
1676 |
|
1677 |
|
1678 |
|
1679 # canonicalize a threshold setting |
|
1680 sub parse_thold_value { |
|
1681 my($pat, $type, $val); |
|
1682 |
|
1683 # check the name of the checksum by converting it into a pattern |
|
1684 # and matching it against the list of checksum types that can have |
|
1685 # per-user thresholds |
|
1686 $pat = ",($_[0]),"; |
|
1687 $pat =~ s/[-_]/[-_]/g; |
|
1688 $type = ',' . $thold_cks . ','; |
|
1689 return 0 if ($type !~ /$pat/i); |
|
1690 $type = $1; |
|
1691 |
|
1692 # check the threshold value |
|
1693 if ($_[1] =~ /^Never$/i) { |
|
1694 $val = 'Never'; |
|
1695 } elsif ($_[1] =~ /^many/i) { |
|
1696 # reputation threshold is a % and reputation total is finite |
|
1697 return 0 if ($type =~ /^rep/); |
|
1698 $val = "many"; |
|
1699 } elsif ($_[1] =~ /^\d+$/) { |
|
1700 $val = $_[1]; |
|
1701 if ($type =~ /^rep$/i) { |
|
1702 return 0 if ($val > 100); |
|
1703 $val .= '%'; |
|
1704 } |
|
1705 } elsif ($_[1] =~ /^(\d+)%$/) { |
|
1706 # reputation threshold is a % |
|
1707 return 0 if ($1 > 100 || $type !~ /^rep$/i); |
|
1708 $val = $_[1]; |
|
1709 } else { |
|
1710 return 0; |
|
1711 } |
|
1712 |
|
1713 |
|
1714 $_[0] = $type; |
|
1715 $_[1] = $val; |
|
1716 return 1; |
|
1717 } |
|
1718 |
|
1719 |
|
1720 |
|
1721 # See if a whiteclnt line makes sense |
|
1722 # If so, return a list of key and canonicalized line. |
|
1723 # If it is an option setting, return a third string that is the value |
|
1724 # for the edit form. |
|
1725 # If not, return only an error message. |
|
1726 sub parse_white_entry { |
|
1727 my($line, # line to parse |
|
1728 $mode # ''=accept from file, |
|
1729 # 'option'=new option setting |
|
1730 # 'strict'=new whitelist entry |
|
1731 ) = @_; |
|
1732 |
|
1733 my($count, $key, $type, $value, $emsg); |
|
1734 |
|
1735 # recognize options |
|
1736 if (!$mode || $mode eq "option") { |
|
1737 return ("dccenable", "option dcc-$1\n", "$1") |
|
1738 if ($line =~ /^\s*option\s+DCC-(on|off)\s*$/i); |
|
1739 |
|
1740 return ("greyfilter", "option greylist-$1\n", "$1") |
|
1741 if ($line =~ /^\s*option\s+greylist-(on|off)\s*$/i); |
|
1742 |
|
1743 return ("greylog", "option greylist-log-$1\n", "$1") |
|
1744 if ($line =~ /^\s*option\s+greylist-log-(on|off)\s*$/i); |
|
1745 |
|
1746 return ("mtafirst", "option MTA-$1\n", "$1") |
|
1747 if ($line =~ /^\s*option\s+MTA-(first|last)\s*$/i); |
|
1748 |
|
1749 return ("rep", "option DCC-rep-$1\n", "$1") |
|
1750 if ($line =~ /^\s*option\s+DCC-reps?-(on|off)\s*$/i); |
|
1751 |
|
1752 return ("dnsbl1", "option dnsbl1-$1\n", "$1") |
|
1753 if ($line =~ /^\s*option\s+dnsbl-(on|off)\s*$/i); |
|
1754 return ("dnsbl$1", "option dnsbl$1-$2\n", "$2") |
|
1755 if ($line =~ /^\s*option\s+dnsbl([123])-(on|off)\s*$/i); |
|
1756 |
|
1757 return ("logall", "option log-all\n", "on") |
|
1758 if ($line =~ /^\s*option\s+log-all\s*$/i); |
|
1759 return ("logall", "option log-normal\n", "off") |
|
1760 if ($line =~ /^\s*option\s+log-normal\s*$/i); |
|
1761 |
|
1762 return ("logsubdir", "option log-subdirectory-$1\n", "$1") |
|
1763 if ($line =~ /^\s*option\s+log-subdirectory-(day|hour|minute)\s*$/i); |
|
1764 |
|
1765 return ("discardok", "option forced-discard-ok\n", "discard spam") |
|
1766 if ($line =~ /^\s*option\s+forced-discard-ok\s*$/i); |
|
1767 return ("discardok", "option no-forced-discard\n", "delay mail") |
|
1768 if ($line =~ /^\s*option\s+no-forced-discard\s*$/i |
|
1769 || $line =~ /^\s*option\s+forced-discard-nok\s*$/i); # obsolete |
|
1770 |
|
1771 if ($line =~ /^\s*option\s+threshold\s+(\S+),(\S+)\s*$/i) { |
|
1772 $type = $1; |
|
1773 $value = $2; |
|
1774 return ("thold-$type", "option threshold $type,$value\n", "$value") |
|
1775 if (parse_thold_value($type, $value)); |
|
1776 } |
|
1777 |
|
1778 # recognize old logging options |
|
1779 return ("greylog", "option greylist-log-on\n", "on") |
|
1780 if ($line =~ /^\s*log\s+all-grey\s*$/i); |
|
1781 return ("greylog", "option greylist-log-off\n", "off") |
|
1782 if ($line =~ /^\s*log\s+no-grey\s*$/i); |
|
1783 |
|
1784 # we are finished if only parsing a new option line we know is ok |
|
1785 return "unrecognized option line" if ($mode && $mode eq "option"); |
|
1786 } |
|
1787 |
|
1788 # must be "" with a bad option or "strict" when we should see an option |
|
1789 return "unrecognized option line" |
|
1790 if ($line =~/^log/i || $line =~ /^option/i); |
|
1791 |
|
1792 return "unrecognized line" if ($line !~ /^(\S+)\s+(.*)/); |
|
1793 $count = $1; |
|
1794 $value = $2; |
|
1795 |
|
1796 return "unrecognized count \"$count\"" if ($count !~ /many|ok|ok2/i); |
|
1797 |
|
1798 ($type, $value, $emsg) = parse_type_value($value); |
|
1799 return $emsg if ($emsg); |
|
1800 |
|
1801 # build the whiteclnt line |
|
1802 $line = "$count\t$type"; |
|
1803 $line .= (length($type) < 8) ? "\t" : ' '; |
|
1804 $line .= "$value\n"; |
|
1805 |
|
1806 $value =~ s/\s//g; |
|
1807 $value =~ tr/A-Z/a-z/; |
|
1808 $key = "$type $value"; |
|
1809 |
|
1810 return ($key, $line); |
|
1811 } |
|
1812 |
|
1813 |
|
1814 |
|
1815 # check a proposed entry |
|
1816 # return an array of the error message if the proposed entry is bogus |
|
1817 # or an array of (key, comment, line) if it make sense |
|
1818 sub ck_new_white_entry { |
|
1819 my($comment, $count, $type, $value) = @_; |
|
1820 my(@parsed, @entry); |
|
1821 |
|
1822 return "missing comment" if (!defined($comment)); |
|
1823 return "missing count" if (!$count); |
|
1824 return "missing type" if (!$type); |
|
1825 return "missing value" if (!$value); |
|
1826 |
|
1827 # trim trailing whitespace from the comment lines |
|
1828 $comment =~ s/\s+\n/\n/g; |
|
1829 # ensure comment lines start with '#' |
|
1830 $comment =~ s/^([ \t]*[^# \t\n])/#$1/gm; |
|
1831 # trim trailing blank lines from the comment |
|
1832 $comment =~ s/\s+$//s; |
|
1833 $comment .= "\n" if (length($comment) != 0); |
|
1834 |
|
1835 @parsed = parse_white_entry("$count $type $value", "strict"); |
|
1836 return ($parsed[0]) if (!defined($parsed[1])); |
|
1837 |
|
1838 $entry[0] = $parsed[0]; |
|
1839 $entry[2] = $parsed[1]; |
|
1840 $entry[1] = $comment; |
|
1841 return @entry; |
|
1842 } |
|
1843 |
|
1844 |
|
1845 |
|
1846 # add, change, or delete a whitelist entry |
|
1847 # write our image of the file to disk, changing it as we go |
|
1848 # then read the file |
|
1849 sub chg_white_entry { |
|
1850 my($file_ref, # the file in memory |
|
1851 $dict_ref, # dictionary for the file |
|
1852 $cur_key, # change or delete this entry |
|
1853 $entry_ref, # change this if not null |
|
1854 $add_pos # add before this if defined |
|
1855 ) = @_; |
|
1856 my($msg, $i, $k, @file); |
|
1857 |
|
1858 return "$whiteclnt locked" if ($whiteclnt_lock =~ /\blocked/); |
|
1859 |
|
1860 @file = @$file_ref; |
|
1861 |
|
1862 if (!${$dict_ref}{$cur_key}) { |
|
1863 # it is a new entry if it exists |
|
1864 if ($entry_ref) { |
|
1865 # add it to the list that will go to the disk |
|
1866 ${$dict_ref}{$cur_key} = @$entry_ref; |
|
1867 if (!$add_pos || $add_pos > $#file) { |
|
1868 # append to the file without a good position |
|
1869 push @file, $entry_ref; |
|
1870 } else { |
|
1871 # insert at the right position |
|
1872 @file = (@file[0 .. $add_pos-1], |
|
1873 $entry_ref, |
|
1874 @file[$add_pos .. $#file]); |
|
1875 } |
|
1876 } |
|
1877 |
|
1878 } else { |
|
1879 # changing or deleting existing entry, so delete duplicates |
|
1880 $i = 0; |
|
1881 while (${$dict_ref}{$k = "DUP-$i-$cur_key"}) { |
|
1882 ${$dict_ref}{$k}[1] = undef; |
|
1883 ++$i; |
|
1884 } |
|
1885 |
|
1886 if (!$entry_ref) { |
|
1887 # delete an entry |
|
1888 ${$dict_ref}{$cur_key}[1] = undef; |
|
1889 |
|
1890 } else { |
|
1891 # change an entry |
|
1892 @{${$dict_ref}{$cur_key}} = @$entry_ref; |
|
1893 } |
|
1894 } |
|
1895 |
|
1896 # put the changes on the disk |
|
1897 $msg = write_whiteclnt(@file); |
|
1898 return $msg if ($msg); |
|
1899 |
|
1900 # set the web form that includes the response |
|
1901 read_whiteclnt($file_ref, $dict_ref); |
|
1902 return undef; |
|
1903 } |
|
1904 |
|
1905 |
|
1906 |
|
1907 # write a new version of the file |
|
1908 sub write_whiteclnt { # return undef or error message |
|
1909 my(@file) = @_; |
|
1910 local(*DIR, *BAK); |
|
1911 my(@baks, $bak, $buf, $entry, $preamble); |
|
1912 |
|
1913 # delete old backup files and find the name of the next one |
|
1914 # keep only the last few and fairly recent revisions |
|
1915 opendir(DIR, "$user_dir") or html_whine("opendir($user_dir): $!"); |
|
1916 @baks = map("$user_dir/$_", |
|
1917 sort grep {/^(whiteclnt\.bak\d+$)/ && -f "$user_dir/$1"} |
|
1918 readdir(DIR)); |
|
1919 closedir(DIR); |
|
1920 while ($#baks > 1 && ($baks[0] =~ /(.*\/whiteclnt\.bak\d+$)/) |
|
1921 && ((-M $1) >= 1 || $#baks >= 19)) { |
|
1922 unlink $1; # suppress taint warning |
|
1923 shift(@baks); |
|
1924 } |
|
1925 if ($#baks >= 0) { |
|
1926 $baks[$#baks] =~ /\/whiteclnt\.bak(\d+)$/; |
|
1927 $bak = sprintf("%s/whiteclnt.bak%06d", $user_dir, $1+1); |
|
1928 } else { |
|
1929 $bak = "$whiteclnt.bak000000"; |
|
1930 } |
|
1931 |
|
1932 # create the undo file and copy the real file to it |
|
1933 # It could be smoother to rename the current file, but we might |
|
1934 # not have permission to create the new file with the correct owner. |
|
1935 # There are also dangers with symbolic links and rename(). |
|
1936 return "cannot create $bak: $!" |
|
1937 if (!sysopen(BAK, $bak, O_WRONLY | O_CREAT | O_EXCL, 0660)); |
|
1938 return "seek($whiteclnt): $!" |
|
1939 if (!seek(WHITECLNT, 0, 0)); |
|
1940 while (read(WHITECLNT, $buf, 8*1024)) { |
|
1941 return "write($bak): $!" |
|
1942 if (!syswrite(BAK, $buf)); |
|
1943 } |
|
1944 close(BAK); |
|
1945 |
|
1946 # rewrite the real file |
|
1947 return "seek($whiteclnt): $!" |
|
1948 if (!seek(WHITECLNT, 0, 0)); |
|
1949 return "truncate($whiteclnt): $!" |
|
1950 if (!truncate(WHITECLNT, 0)); |
|
1951 |
|
1952 $preamble = 0; |
|
1953 foreach $entry (@file) { |
|
1954 # skip deleted entries, |
|
1955 next if (!defined($$entry[1])); |
|
1956 |
|
1957 # put the parameters in the preamble |
|
1958 if (!$preamble) { |
|
1959 $preamble = $$entry[1]; |
|
1960 $whiteclnt_change_log .= strftime("#webuser changed %x %X%n", |
|
1961 localtime); |
|
1962 $preamble =~ s/\n(\n?)$/\n/; |
|
1963 $whiteclnt_change_log .= $1; |
|
1964 print WHITECLNT $preamble; |
|
1965 print WHITECLNT $whiteclnt_version; |
|
1966 print WHITECLNT $whiteclnt_notify; |
|
1967 print WHITECLNT $whiteclnt_lock; |
|
1968 print WHITECLNT "#webuser cur_key $whiteclnt_cur_key\n" |
|
1969 if ($whiteclnt_cur_key); |
|
1970 print WHITECLNT $whiteclnt_change_log; |
|
1971 } else { |
|
1972 print WHITECLNT $$entry[1]; |
|
1973 print WHITECLNT $$entry[2]; |
|
1974 } |
|
1975 } |
|
1976 |
|
1977 return undef; |
|
1978 } |
|
1979 |
|
1980 |
|
1981 |
|
1982 # undo the most recent operation by copying from the newest backup |
|
1983 sub undo_whiteclnt { |
|
1984 my($bak, $buf); |
|
1985 local(*BAK); |
|
1986 |
|
1987 return "$whiteclnt locked" if ($whiteclnt_lock =~ /\blocked/); |
|
1988 |
|
1989 $bak = newest_whiteclnt_bak(); |
|
1990 return "nothing undone" |
|
1991 if (!$bak); |
|
1992 |
|
1993 return "open($bak): $!" |
|
1994 if (!open(BAK, "< $bak")); |
|
1995 |
|
1996 return "seek($whiteclnt): $!" |
|
1997 if (!seek(WHITECLNT, 0, 0)); |
|
1998 return "truncate($whiteclnt): $!" |
|
1999 if (!truncate(WHITECLNT, 0)); |
|
2000 while (read(BAK, $buf, 8*1024)) { |
|
2001 return "write($whiteclnt): $!" |
|
2002 if (!print(WHITECLNT $buf)); |
|
2003 } |
|
2004 |
|
2005 return "unlink($bak): $!" |
|
2006 if (!unlink($bak)); |
|
2007 |
|
2008 return undef; |
|
2009 } |
|
2010 |
|
2011 |
|
2012 |
|
2013 # find the newest backup file |
|
2014 sub newest_whiteclnt_bak { |
|
2015 local(*DIR); |
|
2016 my(@baks, $bak); |
|
2017 |
|
2018 opendir(DIR, "$user_dir") || return undef; |
|
2019 @baks = sort grep {/^whiteclnt\.bak\d+/ && -f "$user_dir/$_"} |
|
2020 readdir(DIR); |
|
2021 closedir(DIR); |
|
2022 |
|
2023 return undef |
|
2024 if ($#baks < 0); |
|
2025 $bak = "$user_dir/$baks[$#baks]"; |
|
2026 return undef |
|
2027 if (-M $bak >= 1); |
|
2028 return undef # suppress taint warning |
|
2029 if ($bak !~ /(.*\/whiteclnt\.bak\d+$)/); |
|
2030 return $1; |
|
2031 } |