Anchor a few regexes in handle_loglevel_connect().
[privoxy.git] / tools / privoxy-log-parser.pl
1 #!/usr/bin/perl
2
3 ################################################################################
4 # privoxy-log-parser
5 #
6 # A parser for Privoxy log messages. For incomplete documentation run
7 # perldoc privoxy-log-parser(.pl), for fancy screenshots see:
8 #
9 # http://www.fabiankeil.de/sourcecode/privoxy-log-parser/
10 #
11 # $Id: privoxy-log-parser.pl,v 1.130 2008/12/22 18:57:59 fk Exp $
12 #
13 # TODO:
14 #       - LOG_LEVEL_CGI, LOG_LEVEL_ERROR, LOG_LEVEL_WRITE content highlighting
15 #       - create fancy statistics
16 #       - grep through Privoxy sources to find unsupported log messages
17 #       - hunt down substitutions that match content from variables which
18 #         can contain stuff like ()?'[]
19 #       - replace $h{'foo'} with h('foo') where possible
20 #       - hunt down XXX comments instead of just creating them
21 #       - add example log lines for every regex and mark them up for
22 #         regression testing
23 #       - Handle incomplete input without Perl warning about undefined variables.
24 #       - Use generic highlighting function that takes a regex and the
25 #         hash key as input.
26 #
27 # Copyright (c) 2007-2008 Fabian Keil <fk@fabiankeil.de>
28 #
29 # Permission to use, copy, modify, and distribute this software for any
30 # purpose with or without fee is hereby granted, provided that the above
31 # copyright notice and this permission notice appear in all copies.
32 #
33 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
34 # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
35 # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
36 # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
37 # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
38 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
39 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
40 ################################################################################
41
42 use strict;
43 use warnings;
44 use Getopt::Long;
45
46 use constant {
47     PRIVOXY_LOG_PARSER_VERSION => '0.4',
48     # Feel free to mess with these ...
49     DEFAULT_BACKGROUND => 'black',  # Choose registered colour (like 'black')
50     DEFAULT_TEXT_COLOUR => 'white', # Choose registered colour (like 'black')
51     HEADER_DEFAULT_COLOUR => 'yellow',
52     REGISTER_HEADERS_WITH_THE_SAME_COLOUR => 1,
53
54     CLI_OPTION_DEFAULT_TO_HTML_OUTPUT => 0,
55     CLI_OPTION_TITLE => 'Privoxy-Log-Parser in da house',
56     CLI_OPTION_NO_EMBEDDED_CSS => 0,
57     CLI_OPTION_NO_MSECS => 0,
58     CLI_OPTION_NO_SYNTAX_HIGHLIGHTING => 0,
59     CLI_OPTION_ERROR_LOG_FILE => '/var/log/privoxy-log.log',
60     CLI_OPTION_SHOW_INEFFECTIVE_FILTERS => 0,
61     CLI_OPTION_ACCEPT_UNKNOWN_MESSAGES => 0,
62     CLI_OPTION_STATISTIC => 0,
63
64     SUPPRESS_SUCCEEDED_FILTER_ADDITIONS => 1,
65     SHOW_SCAN_INTRO => 0,
66     SHOW_FILTER_READIN_IN => 0,
67     SUPPRESS_EMPTY_LINES => 1,
68     SUPPRESS_SUCCESSFUL_CONNECTIONS => 1,
69     SUPPRESS_ACCEPTED_CONNECTIONS => 1,
70     SUPPRESS_GIF_NOT_CHANGED => 1,
71     SUPPRESS_NEED_TO_DE_CHUNK_FIRST => 1,
72
73     DEBUG_HEADER_REGISTERING => 0,
74     DEBUG_HEADER_HIGHLIGHTING => 0,
75     DEBUG_TICKS => 0,
76     DEBUG_PAINT_IT => 0,
77     DEBUG_SUPPRESS_LOG_MESSAGES => 0,
78
79     PUNISH_MISSING_LOG_KNOWLEDGE_WITH_DEATH => 0,
80     PUNISH_MISSING_HIGHLIGHT_KNOWLEDGE_WITH_DEATH => 1,
81
82     LOG_UNPARSED_LINES_TO_EXTRA_FILE => 0,
83
84     # You better leave these alone unless you know what you're doing.
85     COLOUR_RESET      => "\033[0;0m",
86     ESCAPE => "\033[",
87 };
88
89 sub prepare_our_stuff () {
90
91     # Syntax Higlight hash
92     our @all_colours = (
93         'red', 'green', 'brown', 'blue', 'purple', 'cyan',
94         'light_gray', 'light_red', 'light_green', 'yellow',
95         'light_blue', 'pink', 'light_cyan', 'white'
96     );
97
98     our %h = (
99         # LOG_LEVEL
100         Info            => 'blue',
101         Header          => 'green',
102         Filter          => 'purple', # XXX: Used?
103         'Re-Filter'     => 'purple',
104         Connect         => 'brown',
105         Request         => 'light_cyan',
106         CGI             => 'light_green',
107         Redirect        => 'cyan',
108         Error           => 'light_red',
109         Crunch          => 'cyan',
110         'Fatal error'   => 'light_red',
111         'Gif-Deanimate' => 'blue',
112         Force           => 'red',
113         Writing         => 'light_green',
114         # ----------------------
115         URL                  => 'yellow',
116         path                 => 'brown',
117         request_             => 'brown', # host+path but no protocol
118         'ip-address'         => 'yellow',
119         Number               => 'yellow',
120         Standard             => 'reset',
121         Truncation           => 'light_red',
122         Status               => 'brown',
123         Timestamp            => 'brown',
124         Crunching            => 'light_red',
125         crunched             => 'light_red',
126         'Request-Line'       => 'pink',
127         method               => 'purple',
128         destination          => 'yellow',
129         'http-version'       => 'pink',
130         'crunch-pattern'     => 'pink',
131         not                  => 'brown',
132         file                 => 'brown',
133         signal               => 'yellow',
134         version              => 'green',
135         'program-name'       => 'cyan',
136         port                 => 'red',
137         host                 => 'red',
138         warning              => 'light_red',
139         debug                => 'light_red',
140         filter               => 'green',
141         tag                  => 'green',
142         tagger               => 'green',
143         'status-message'     => 'light_cyan',
144         'status-code'        => 'yellow',
145         'invalid-request'    => 'light_red',
146         'hits'               => 'yellow',
147         error                => 'light_red',
148         'rewritten-URL'      => 'light_red',
149         'pcrs-delimiter'     => 'light_red',
150         'ignored'            => 'light_red',
151         'action-bits-update' => 'light_red',
152         'configuration-line' => 'red',
153         'content-type'       => 'yellow',
154     );
155
156     our %h_colours = %h;
157
158     # Header colours need their own hash so the keys can be accessed properly
159     our %header_colours = (
160         # Prefilled with headers that should not appear with default header colours
161         Cookie => 'light_red',
162         'Set-Cookie' => 'light_red',
163         Warning => 'light_red',
164         Default => HEADER_DEFAULT_COLOUR,
165     );
166
167     # Crunch reasons need their own hash as well
168     our %reason_colours = (
169         'Unsupported HTTP feature'               => 'light_red',
170         Blocked                                  => 'light_red',
171         Untrusted                                => 'light_red',
172         Redirected                               => 'green', 
173         'CGI Call'                               => 'white',
174         'DNS failure'                            => 'red',
175         'Forwarding failed'                      => 'light_red',
176         'Connection failure'                     => 'light_red',
177         'Out of memory (may mask other reasons)' => 'light_red',
178         'No reason recorded'                     => 'light_red',
179     );
180
181     our @time_colours = ('white', 'light_gray');
182
183     # Translate highlight strings into highlight code
184     prepare_highlight_hash(\%header_colours);
185     prepare_highlight_hash(\%reason_colours);
186     prepare_highlight_hash(\%h);
187     prepare_colour_array(\@all_colours);
188     prepare_colour_array(\@time_colours);
189     init_css_colours();
190 }
191
192 sub paint_it ($) {
193 ###############################################################
194 # Takes a colour string and returns an ANSI escape sequence
195 # (unless --no-syntax-highlighting is used).
196 # XXX: The Rolling Stones reference has to go.
197 ###############################################################
198
199     my $colour = shift @_;
200
201     return "" if cli_option_is_set('no-syntax-highlighting');
202
203     my %light = (
204         black       => 0,    
205         red         => 0,   
206         green       => 0,  
207         brown       => 0, 
208         blue        => 0,   
209         purple      => 0, 
210         cyan        => 0,  
211         light_gray  => 0,
212         gray        => 0,
213         dark_gray   => 1,
214         light_red   => 1,
215         light_green => 1,
216         yellow      => 1,
217         light_blue  => 1,
218         pink        => 1,
219         light_cyan  => 1,
220         white       => 1,
221     );
222
223     my %text = (
224         black       => 30,    
225         red         => 31,   
226         green       => 32,  
227         brown       => 33, 
228         blue        => 34,   
229         purple      => 35, 
230         cyan        => 36,  
231         gray        => 37,
232         light_gray  => 37,
233         dark_gray   => 30,
234         light_red   => 31,
235         light_green => 32,
236         yellow      => 33,
237         light_blue  => 34,
238         pink        => 35,
239         light_cyan  => 36,
240         white       => 37,
241     );
242
243     my $bg_code = get_background();
244     my $colour_code;
245     our $default = default_colours();
246
247     if (defined($text{$colour})) {
248         $colour_code  = ESCAPE;
249         $colour_code .= $text{$colour};
250         $colour_code .= ";";
251         $colour_code .= $light{$colour} ? "1" : "2";
252         $colour_code .= ";";
253         $colour_code .= $bg_code; 
254         $colour_code .= "m";
255         debug_message $colour . " is \'" . $colour_code . $colour . $default . "\'" if DEBUG_PAINT_IT; 
256
257     } elsif ($colour =~ /reset/) {
258
259         $colour_code = default_colours();
260
261     } else {
262
263         die "What's $colour supposed to mean?\n"; 
264     }
265
266     return $colour_code;
267 }
268
269 sub get_semantic_html_markup ($) {
270 ###############################################################
271 # Takes a string and returns a span element
272 ###############################################################
273
274     my $type = shift @_;
275     my $code;
276
277     if ($type =~ /Standard/) {
278         $code = '</span>';
279     } else {
280         $type = lc($type);
281         $code = '<span title="' . $type . '" class="' . $type . '">';
282     }
283
284     return $code;
285 }
286
287 sub cli_option_is_set ($) {
288
289     our %cli_options;
290     my $cli_option = shift;
291
292     die "Unknown CLI option: $cli_option" unless defined $cli_options{$cli_option};
293
294     return $cli_options{$cli_option};
295 }
296
297 sub get_html_title () {
298
299     our %cli_options;
300     return $cli_options{'title'};
301
302 }
303
304 sub init_css_colours() {
305
306     our %css_colours = (
307         black       => "000",    
308         red         => "F00",   
309         green       => "0F0",  
310         brown       => "C90", 
311         blue        => "0F0",   
312         purple      => "F06", # XXX: wrong  
313         cyan        => "F09", # XXX: wrong  
314         light_gray  => "999",
315         gray        => "333",
316         dark_gray   => "222",
317         light_red   => "F33",
318         light_green => "33F",
319         yellow      => "FF0",
320         light_blue  => "30F",
321         pink        => "F0F",
322         light_cyan  => "66F",
323         white       => "FFF",
324     );
325 }
326
327 sub get_css_colour ($) {
328
329    our %css_colours;
330    my $colour = shift;
331
332    die "What's $colour supposed to mean?\n" unless defined($css_colours{$colour}); 
333
334    return '#' . $css_colours{$colour};
335 }
336
337 sub get_css_line ($) {
338
339     our %h_colours;
340
341     my $class = shift;
342     my $css_line;
343
344     $css_line .= '.' . lc($class) . ' {'; # XXX: lc() shouldn't be necessary
345     die "What's $class supposed to mean?\n" unless defined($h_colours{$class}); 
346     $css_line .= 'color:' . get_css_colour($h_colours{$class}) . ';';
347     $css_line .= 'background-color:' . get_css_colour(DEFAULT_BACKGROUND) . ';';
348     $css_line .= '}' . "\n"; 
349
350     return $css_line;
351 }
352
353 sub get_css_line_for_colour ($) {
354
355     our %h_colours;
356
357     my $colour = shift;
358     my $css_line;
359
360     $css_line .= '.' . lc($colour) . ' {'; # XXX: lc() shouldn't be necessary
361     $css_line .= 'color:' . get_css_colour($colour) . ';';
362     $css_line .= 'background-color:' . get_css_colour(DEFAULT_BACKGROUND) . ';';
363     $css_line .= '}' . "\n"; 
364
365     return $css_line;
366 }
367
368 # XXX: Wrong solution
369 sub get_missing_css_lines () {
370
371     my $css_line;
372
373     $css_line .= '.' . 'default' . ' {'; # XXX: lc() shouldn't be necessary
374     $css_line .= 'color:' . HEADER_DEFAULT_COLOUR . ';';
375     $css_line .= 'background-color:' . get_css_colour(DEFAULT_BACKGROUND) . ';';
376     $css_line .= '}' . "\n"; 
377
378     return $css_line;
379 }
380
381 sub get_css () {
382
383     our %h_colours;
384     our %css_colours; #XXX: Wrong solution
385
386     my $css = '';
387
388     $css .= '.privoxy-log {';
389     $css .= 'color:' . get_css_colour(DEFAULT_TEXT_COLOUR) . ';';
390     $css .= 'background-color:' . get_css_colour(DEFAULT_BACKGROUND) . ';';
391     $css .= '}' . "\n"; 
392  
393     foreach my $key (keys %h_colours) {
394
395         next if ($h_colours{$key} =~ m/reset/); #XXX: Wrong solution.
396         $css .= get_css_line($key);
397
398     }
399
400     foreach my $colour (keys %css_colours) {
401
402         $css .= get_css_line_for_colour($colour);
403
404     }
405
406     $css .= get_missing_css_lines(); #XXX: Wrong solution
407
408     return $css;
409 }
410
411 sub print_intro () {
412
413     my $intro = '';
414
415     if (cli_option_is_set('html-output')) {
416
417         my $title = get_html_title();
418
419         $intro .= '<html><head>';
420         $intro .= '<title>' . $title . '</title>';
421         $intro .= '<style>' . get_css() . '</style>' unless cli_option_is_set('no-embedded-css');
422         $intro .= '</head><body>';
423         $intro .= '<h1>' . $title . '</h1><p class="privoxy-log">';
424
425         print $intro;
426     }
427 }
428
429 sub print_outro () {
430
431     my $outro = '';
432
433     if (cli_option_is_set('html-output')) {
434
435         $outro = '</p></body></html>';
436         print $outro;
437
438     }
439 }
440
441 sub get_line_end () {
442
443     my $line_end = "\n";
444
445     $line_end = '<br>' . $line_end if cli_option_is_set('html-output');
446
447     return $line_end;
448 }
449
450 sub get_colour_html_markup ($) {
451 ###############################################################
452 # Takes a colour string a span element. XXX: WHAT?
453 # XXX: This function shouldn't be necessary, the
454 # markup should always be semantically correct.
455 ###############################################################
456
457     my $type = shift @_;
458     my $code;
459
460     if ($type =~ /Standard/) {
461         $code = '</span>';
462     } else {
463         $code = '<span class="' . lc($type) . '">';
464     }
465
466     return $code;
467 }
468
469 sub default_colours () {
470     # XXX: Properly
471     our $bg_code;
472     return reset_colours();
473 }
474
475 sub show_colours () {
476     # XXX: Implement
477 }
478
479 sub reset_colours () {
480     return ESCAPE . "0m";
481 }
482
483 sub set_background ($){
484
485     my $colour = shift;
486     our $bg_code;
487     my %backgrounds = (
488               black       => "40",    
489               red         => "41",   
490               green       => "42",  
491               brown       => "43", 
492               blue        => "44",   
493               magenta     => "45",  
494               cyan        => "46",
495               white       => "47",  
496               default     => "49",  
497     );
498     
499     if (defined($backgrounds{$colour})) {
500         $bg_code = $backgrounds{$colour};
501     } else {
502         die "Invalid background colour: " . $colour;
503     }
504 }
505
506 sub get_background (){
507     return our $bg_code;
508 }
509
510 sub prepare_highlight_hash ($) {
511     my $ref = shift;
512
513     if (!cli_option_is_set('html-output')) {
514
515         foreach my $key (keys %$ref) {
516             $$ref{$key} = paint_it($$ref{$key}); 
517         }
518
519     } else {
520
521         foreach my $key (keys %$ref) {
522             $$ref{$key} = get_semantic_html_markup($key); 
523         }
524
525     } 
526 }
527
528 sub prepare_colour_array ($) {
529     my $ref = shift;
530
531     if (!cli_option_is_set('html-output')) {
532
533         foreach my $i (0 ... @$ref - 1) {
534             $$ref[$i] = paint_it($$ref[$i]); 
535         } 
536
537     } else {
538
539         foreach my $i (0 ... @$ref - 1) {
540             $$ref[$i] = get_colour_html_markup($$ref[$i]);
541         } 
542
543     }
544 }
545
546 sub found_unknown_content ($) {
547
548     my $unknown = shift;
549     my $message;
550
551     our %req;
552     our $t;
553
554     return if cli_option_is_set('accept-unknown-messages');
555
556     return if ($unknown =~ /\[too long, truncated\]$/);
557
558     $message = "found_unknown_content: Don't know how to highlight: ";
559     # Break line so the log file can later be parsed as Privoxy log file again
560     $message .= '"' . $unknown . '"' . " in:\n";
561     $message .= $req{$t}{'log-message'};
562     debug_message($message);
563     log_parse_error($req{$t}{'log-message'});
564
565     die "Unworthy content parser" if PUNISH_MISSING_LOG_KNOWLEDGE_WITH_DEATH;
566 }
567
568 sub log_parse_error ($) {
569
570     my $message = shift;
571
572     if (LOG_UNPARSED_LINES_TO_EXTRA_FILE) {
573         open(ERRORLOG, ">>" . ERROR_LOG_FILE) || die "Writing " . ERROR_LOG_FILE . " failed";
574         print ERRORLOG $message;
575         close(ERRORLOG);
576     }
577 }
578
579 sub debug_message (@) {
580     my @message = @_;
581     our %h;
582
583     print $h{'debug'} . "@message" . $h{'Standard'} . "\n";
584 }
585
586 ################################################################################
587 # highlighter functions that aren't loglevel-specific 
588 ################################################################################
589
590 sub h ($) {
591
592     # Get highlight marker
593     our %h;
594     my $highlight = shift; # XXX: Stupid name;
595     my $result = '';
596     my $message;
597
598     if (defined($highlight)) {
599
600         $result = $h{$highlight};
601
602     } else {
603
604         $message = "h: Don't recognize highlighter $highlight.";
605         debug_message($message);
606         log_parser_error($message);
607         die "Unworthy highlighter function" if PUNISH_MISSING_HIGHLIGHT_KNOWLEDGE_WITH_DEATH;
608     }
609    
610     return $result;
611 }
612
613 sub highlight_known_headers ($) {
614
615     my $content = shift;
616     our %header_colours;
617     our %h;
618     my $headers = join ('|', keys %header_colours);
619
620     debug_message("Searching $content for things to highlight.") if DEBUG_HEADER_HIGHLIGHTING;
621
622     if ($content =~ m/(?<=\s)($headers):/) {
623         my $header = $1;
624         $content =~ s@(?<=[\s|'])($header)(?=:)@$header_colours{$header}$1$h{'Standard'}@ig;
625         debug_message("Highlighted $content") if DEBUG_HEADER_HIGHLIGHTING;
626     }
627
628     return $content;
629 }
630
631 sub highlight_matched_request_line ($$) {
632
633     my $result = shift; # XXX: Stupid name;
634     my $regex = shift;
635     if ($result =~ m@(.*)($regex)(.*)@) {
636         $result = $1 . highlight_request_line($2) . $3
637     }
638     return $result;
639 }
640
641 sub highlight_request_line ($) {
642
643     my $rl = shift;
644     my ($method, $url, $http_version);
645     our %h;
646
647     #GET http://images.sourceforge.net/sfx/icon_warning.gif HTTP/1.1
648     if ($rl =~ m/Invalid request/) {
649
650         $rl = h('invalid-request') . $rl . h('Standard');
651
652     } elsif ($rl =~ m/^([-\w]+) (.*) (HTTP\/\d\.\d)/) {
653
654         # XXX: might not match in case of HTTP method fuzzing.
655         # XXX: save these: ($method, $path, $http_version) = ($1, $2, $3);
656         $rl =~ s@^(\w+)@$h{'method'}$1$h{'Standard'}@;
657         if ($rl =~ /http:\/\//) {
658             $rl = highlight_matched_url($rl, '[^\s]*(?=\sHTTP)');
659         } else {
660             $rl = highlight_matched_pattern($rl, 'request_', '[^\s]*(?=\sHTTP)');
661         }
662
663         $rl =~ s@(HTTP\/\d\.\d)$@$h{'http-version'}$1$h{'Standard'}@;
664
665     } elsif ($rl =~ m/\.\.\. \[too long, truncated\]$/) {
666
667         $rl =~ s@^(\w+)@$h{'method'}$1$h{'Standard'}@;
668         $rl = highlight_matched_url($rl, '[^\s]*(?=\.\.\.)');
669
670     } elsif ($rl =~ m/^ $/) {
671
672         $rl = h('error') . "No request line specified!" . h('Standard');
673
674     } else {
675
676         debug_message ("Can't parse request line: $rl");
677
678     }
679
680     return $rl;
681 }
682
683 sub highlight_response_line ($) {
684
685     my $rl = shift;
686     my ($http_version, $status_code, $status_message);
687
688     #HTTP/1.1 200 OK
689     #ICY 200 OK
690
691     # TODO: Mark different status codes differently
692
693     if ($rl =~ m/((?:HTTP\/\d\.\d|ICY)) (\d+) (.*)/) {
694         ($http_version, $status_code, $status_message) = ($1, $2, $3);
695     } else {
696         debug_message ("Can't parse response line: $rl") and die 'Fix this';
697     }
698
699     # Rebuild highlighted
700     $rl= "";
701     $rl .= h('http-version') . $http_version . h('Standard');
702     $rl .= " ";
703     $rl .= h('status-code') . $status_code . h('Standard');
704     $rl .= " ";
705     $rl .= h('status-message') . $status_message . h('Standard');
706
707     return $rl;
708 }
709
710 sub highlight_matched_url ($$) {
711
712     my $result = shift; # XXX: Stupid name;
713     my $regex = shift;
714
715     #print "Got $result, regex ($regex)\n";
716
717     if ($result =~ m@(.*?)($regex)(.*)@) {
718         $result = $1 . highlight_url($2) . $3;
719         #print "Now the result is $result\n";
720     }
721
722     return $result;
723 }
724
725 sub highlight_matched_host ($$) {
726
727     my $result = shift; # XXX: Stupid name;
728     my $regex = shift;
729
730     if ($result =~ m@(.*?)($regex)(.*)@) {
731         $result = $1 . h('host') . $2 . h('Standard') . $3;
732     }
733
734     return $result;
735 }
736
737 sub highlight_matched_pattern ($$$) {
738
739     our %h;
740     my $result = shift; # XXX: Stupid name;
741     my $key = shift;
742     my $regex = shift;
743
744     die "Unknown key $key" unless defined $h{$key};
745
746     if ($result =~ m@(.*?)($regex)(.*)@) {
747         $result = $1 . h($key) . $2 . h('Standard') . $3;
748     }
749
750     return $result;
751 }
752
753 sub highlight_matched_path ($$) {
754
755     my $result = shift; # XXX: Stupid name;
756     my $regex = shift;
757
758     if ($result =~ m@(.*?)($regex)(.*)@) {
759         $result = $1 . h('path') . $2 . h('Standard') . $3;
760     }
761
762     return $result;
763 }
764
765 sub highlight_url ($) {
766
767     my $url = shift;
768
769     if (cli_option_is_set('html-output')) {
770
771         $url = '<a href="' . $url . '">' . $url . '</a>';
772
773     } else {
774
775         $url = h('URL') . $url . h('Standard');
776
777     }
778
779     return $url;
780 }
781
782 ################################################################################
783 # loglevel-specific highlighter functions
784 ################################################################################
785
786 sub handle_loglevel_header ($) {
787
788     my $content = shift;
789     my $c = $content;
790     our $t;
791     our %req;
792     our %h;
793     our %header_colours;
794     our @all_colours;
795     our $header_colour_index;
796     our $no_special_header_highlighting;
797
798     # Register new headers
799     # scan: Accept: image/png,image/*;q=0.8,*/*;q=0.5
800     if ($c =~ m/^scan: ((?>[^:]+)):/) {
801         my $header = $1;
802         if (!defined($header_colours{$header})) {
803             debug_message "Registering previously unknown header $1" if DEBUG_HEADER_REGISTERING;
804
805             if (REGISTER_HEADERS_WITH_THE_SAME_COLOUR) {
806                 $header_colours{$header} =  $header_colours{'Default'};
807             } else {
808                 $header_colours{$header} = $all_colours[$header_colour_index % @all_colours];
809                 $header_colour_index++;
810             }
811         }
812     }
813
814     if ($c =~ m/^scan: ((\w*) (.*) (HTTP\/\d\.\d))/) {
815
816             # Client request line
817             # Save for statistics (XXX: Not implemented yet)
818             $req{$t}{'method'} = $2;
819             $req{$t}{'destination'} = $3;
820             $req{$t}{'http-version'} = $4;
821
822             $content = highlight_request_line($1);
823
824     } elsif ($c =~ m/^(scan: )((?:HTTP\/\d\.\d|ICY) (\d+) (.*))/) {
825
826             # Server response line
827             $req{$t}{'response_line'} = $2;
828             $req{$t}{'status_code'} = $3;
829             $req{$t}{'status_message'} = $4;
830             $content = $1 . highlight_response_line($req{$t}{'response_line'});
831
832     } elsif ($c =~ m/^Crunching (?:server|client) header: .* \(contains: ([^\)]*)\)/) {
833
834         # Crunching server header: Set-Cookie: trac_form_token=d5308c34e16d15e9e301a456; (contains: Cookie:)
835         $content =~ s@(?<=contains: )($1)@$h{'crunch-pattern'}$1$h{'Standard'}@;
836         $content =~ s@(Crunching)@$h{$1}$1$h{'Standard'}@;    
837
838     } elsif ($c =~ m/^New host is: ([^\s]*)\./) {
839
840         # New host is: trac.vidalia-project.net. Crunching Referer: http://www.vidalia-project.net/
841         $c = highlight_matched_host($c, '(?<=New host is: )[^\s]+');
842         $content = highlight_matched_url($c, '(?<=Crunching Referer: )[^\s]+');
843
844     } elsif ($c =~ m/^Text mode enabled by force. (Take cover)!/) {
845
846         # Text mode enabled by force. Take cover!
847         $content =~ s@($1)@$h{'warning'}$1$h{'Standard'}@;
848
849     } elsif ($c =~ m/^(New HTTP Request-Line: )(.*)/) {
850
851         # New HTTP Request-Line: GET http://www.privoxy.org/ HTTP/1.1
852         $content = $1 . highlight_request_line($2);
853
854     } elsif ($c =~ m/^Adjust(ed)? Content-Length to \d+/) {
855
856         # Adjusted Content-Length to 2132
857         # Adjust Content-Length to 33533
858         $content =~ s@(?<=Content-Length to )(\d+)@$h{'Number'}$1$h{'Standard'}@;
859         $content = highlight_known_headers($content);
860
861     } elsif ($c =~ m/^Destination extracted from "Host:" header. New request URL:/) {
862
863         # Destination extracted from "Host:" header. New request URL: http://www.cccmz.de/~ridcully/blog/
864         $content = highlight_matched_url($content, '(?<=New request URL: ).*');
865
866     } elsif ($c =~ m/^Couldn\'t parse:/) {
867
868         # XXX: These should probable be logged with LOG_LEVEL_ERROR
869         # Couldn't parse: If-Modified-Since: Wed, 21 Mar 2007 16:34:50 GMT (crunching!)
870         # Couldn't parse: at, 24 Mar 2007 13:46:21 GMT in If-Modified-Since: Sat, 24 Mar 2007 13:46:21 GMT (crunching!)
871         $content =~ s@^(Couldn\'t parse)@$h{'error'}$1$h{'Standard'}@;
872
873     } elsif ($c =~ /^Tagger \'([^\']*)\' added tag \'([^\']*)\'/ or
874              $c =~ m/^Adding tag \'([^\']*)\' created by header tagger \'([^\']*)\'/) {
875
876         # Adding tag 'GET request' created by header tagger 'method-man' (XXX: no longer used)
877         # Tagger 'revalidation' added tag 'REVALIDATION-REQUEST'. No action bit update necessary.
878         # Tagger 'revalidation' added tag 'REVALIDATION-REQUEST'. Action bits updated accordingly.
879
880         # XXX: Save tag and tagger
881
882         $content =~ s@(?<=^Tagger \')([^\']*)@$h{'tagger'}$1$h{'Standard'}@;
883         $content =~ s@(?<=added tag \')([^\']*)@$h{'tag'}$1$h{'Standard'}@;
884         $content =~ s@(?<=Action bits )(updated)@$h{'action-bits-update'}$1$h{'Standard'}@;
885         $no_special_header_highlighting = 1;
886
887     } elsif ($c =~ /^Tagger \'([^\']*)\' didn['']t add tag \'([^\']*)\'/) {
888
889         # Tagger 'revalidation' didn't add tag 'REVALIDATION-REQUEST'. Tag already present
890         # XXX: Save tag and tagger
891
892         $content =~ s@(?<=^Tagger \')([^\']*)@$h{'tag'}$1$h{'Standard'}@;
893         $content =~ s@(?<=didn['']t add tag \')([^\']*)@$h{'tagger'}$1$h{'Standard'}@;
894
895     } elsif ($c =~ m/^(?:scan:|Randomiz|addh:|Adding:|Removing:|Referer:|Modified:|Accept-Language header|[Cc]ookie)/
896           or $c =~ m/^(Text mode is already enabled|Denied request with NULL byte|Replaced:|add-unique:)/
897           or $c =~ m/^(Crunched (incoming|outgoing) cookie|Suppressed offer|Accepted the client)/
898           or $c =~ m/^(addh-unique|Referer forged to)/
899           or $c =~ m/^Downgraded answer to HTTP\/1.0/
900           or $c =~ m/^Parameter: \+hide-referrer\{[^\}]*\} is a bad idea, but I don\'t care./
901           or $c =~ m/^Referer (?:overwritten|replaced) with: Referer: / #XXX: should this be highlighted?
902           or $c =~ m/^Referer crunched!/
903           or $c =~ m/^crunched x-forwarded-for!/
904           or $c =~ m/^crunched From!/
905           or $c =~ m/^ modified$/
906           or $c =~ m/^Content filtering is enabled. Crunching:/
907           or $c =~ m/^force-text-mode overruled the client/
908           or $c =~ m/^Server time in the future\./
909           or $c =~ m/^content-disposition header crunched and replaced with:/i
910           or $c =~ m/^Reducing white space in /
911           or $c =~ m/^Ignoring single quote in /
912           or $c =~ m/^Converting tab to space in /
913           or $c =~ m/A HTTP\/1\.1 response without/
914           or $c =~ m/Disabled filter mode on behalf of the client/
915             )
916     {
917         # XXX: Some of these may need highlighting
918
919         # Modified: User-Agent: Mozilla/5.0 (X11; U; SunOS i86pc; pl-PL; rv:1.8.1.1) Gecko/20070214 Firefox/2.0.0.1
920         # Accept-Language header crunched and replaced with: Accept-Language: pl-pl
921         # cookie 'Set-Cookie: eZSessionCookie=07bfec287c197440d299f81580593c3d; \
922         #  expires=Thursday, 12-Apr-07 15:16:18 GMT; path=/' send by \
923         #  http://wirres.net/article/articleview/4265/1/6/ appears to be using time format 1 (XXX: gone with the wind)
924         # Cookie rewritten to a temporary one: Set-Cookie: NSC_gffe-iuuq-mc-wtfswfs=8efb33a53660;path=/
925         # Text mode is already enabled
926         # Denied request with NULL byte(s) turned into line break(s)
927         # Replaced: 'Connection: Yo, home to Bel Air' with 'Connection: close'
928         # addh-unique: Host: people.freebsd.org
929         # Suppressed offer to compress content
930         # Crunched incoming cookie -- yum!
931         # Accepted the client's request to fetch without filtering.
932         # Crunched outgoing cookie: Cookie: PREF=ID=6cf0abd347b30262:TM=1173357617:LM=1173357617:S=jZypyyJ7LPiwFi1_
933         # addh-unique: Host: subkeys.pgp.net:11371
934         # Referer forged to: Referer: http://10.0.0.1/
935         # Downgraded answer to HTTP/1.0
936         # Parameter: +hide-referrer{pille-palle} is a bad idea, but I don't care.
937         # Referer overwritten with: Referer: pille-palle
938         # Referer replaced with: Referer: pille-palle
939         # crunched x-forwarded-for!
940         # crunched From!
941         #  modified # XXX: pretty stupid log message
942         # Content filtering is enabled. Crunching: 'Range: 1234-5678' to prevent range-mismatch problems
943         # force-text-mode overruled the client's request to fetch without filtering!
944         # Server time in the future.
945         # content-disposition header crunched and replaced with: content-disposition: filename=baz
946         # Content-Disposition header crunched and replaced with: content-disposition: filename=baz
947         # Reducing white space in 'X-LWS-Test: "This  is  quoted" this is not "this  is  " but " this again   is  not'
948         # Ignoring single quote in 'X-LWS-Test: "This  is  quoted" this is not "this  is  " but "  this again   is  not'
949         # Converting tab to space in 'X-LWS-Test:   "This  is  quoted" this   is  not "this  is  "  but  "\
950         #  this again   is  not'
951         # A HTTP/1.1 response without Connection header implies keep-alive.
952         # Disabled filter mode on behalf of the client.
953
954     } elsif ($c =~ m/^scanning headers for:/) {
955
956         return '' unless SHOW_SCAN_INTRO;
957
958     } elsif ($c =~ m/^[Cc]runch(ing|ed)|crumble crunched:/) {
959         # crunched User-Agent!
960         # Crunching: Content-Encoding: gzip
961
962         $content =~ s@(Crunching|crunched)@$h{$1}$1$h{'Standard'}@;
963
964     } elsif ($c =~ m/^Offending request data with NULL bytes turned into \'°\' characters:/) {
965         
966         # Offending request data with NULL bytes turned into '°' characters: Â°Â°n°°(°°°
967
968         $content = h('warning') . $content . h('Standard');
969  
970     } elsif ($c =~ m/^(Transforming \")(.*?)(\" to \")(.*?)(\")/) {
971
972         # Transforming "Proxy-Authenticate: Basic realm="Correos Proxy Server"" to\
973         #  "Proxy-Authenticate: Basic realm="Correos Proxy Server""
974
975        $content =~ s@(?<=^Transforming \")(.*)(?=\" to)@$h{'Header'}$1$h{'Standard'}@;
976        $content =~ s@(?<=to \")(.*)(?=\")@$h{'Header'}$1$h{'Standard'}@;
977
978     } elsif ($c =~ m/^Removing empty header/) {
979
980         # Removing empty header
981         # Ignore for now
982
983     } elsif ($c =~ m/^Content-Type: .* not replaced/) {
984
985         # Content-Type: application/octet-stream not replaced. It doesn't look like text.\
986         #  Enable force-text-mode if you know what you're doing.
987         # XXX: Could highlight more here.
988         $content =~ s@(?<=^Content-Type: )(.*)(?= not replaced)@$h{'content-type'}$1$h{'Standard'}@;
989
990     } else {
991
992         found_unknown_content($content);
993     }
994
995     # Highlight headers   
996     unless ($c =~ m/^Transforming/) {
997         $content = highlight_known_headers($content) unless $no_special_header_highlighting;
998     }
999
1000     return $content;
1001 }
1002
1003 sub handle_loglevel_re_filter ($) {
1004
1005     my $content = shift;
1006     my $c = $content;
1007     my $key;
1008     our $t;
1009     our %req;
1010     our %h;
1011     our %header_colours;
1012     our @all_colours;
1013     our $header_colour_index;
1014
1015     if ($c =~ /\.{3}$/
1016         and $c =~ m/^(?:re_)?filtering \'?(.*?)\'? \(size (\d*)\) with (?:filter )?\'?([^\s]*?)\'? ?\.{3}$/) {
1017
1018         # Used by Privoxy 3.0.5 and 3.0.6:
1019         # XXX: Fill in ...
1020         # Used by Privoxy 3.0.7:
1021         # filtering 'Connection: close' (size 17) with 'generic-content-ads' ...
1022
1023         $req{$t}{'filtered_header'} = $1;
1024         $req{$t}{'old_header_size'} = $2;
1025         $req{$t}{'header_filter_name'} = $3;
1026
1027         unless (cli_option_is_set('show-ineffective-filters') or
1028                 $req{$t}{'header_filter_name'} =~ m/^privoxy-filter-test$/) {
1029             return '';
1030         }
1031         $content =~ s@(?<=\(size )(\d+)@$h{'Number'}$1$h{'Standard'}@;   
1032         $content =~ s@($req{$t}{'header_filter_name'})@$h{'filter'}$1$h{'Standard'}@;
1033
1034     } elsif ($c =~ m/^ ?\.\.\. ?produced (\d*) hits \(new size (\d*)\)\./) {
1035
1036         # ...produced 0 hits (new size 23).
1037         #... produced 1 hits (new size 54).
1038
1039         $req{$t}{'header_filter_hits'} = $1;
1040         $req{$t}{'new_header_size'} = $2;
1041
1042         unless (cli_option_is_set('show-ineffective-filters') or
1043                 (defined($req{$t}{'header_filter_name'}) and
1044                  $req{$t}{'header_filter_name'} =~ m/^privoxy-filter-test$/)) {
1045
1046             if ($req{$t}{'header_filter_hits'} == 0 and
1047                 not (defined($req{$t}{'header_filter_name'}) and
1048                  $req{$t}{'header_filter_name'} =~ m/^privoxy-filter-test$/)) {
1049                 return ''; 
1050             }
1051             # Reformat including information from the intro
1052             $c = "'" . h('filter') . $req{$t}{'header_filter_name'} . h('Standard') . "'";
1053             $c .= " hit ";
1054             # XXX: Hide behind constant, it may be interesting if LOG_LEVEL_HEADER isn't enabled as well.
1055             # $c .= $req{$t}{'filtered_header'} . " ";
1056             $c .= h('Number') . $req{$t}{'header_filter_hits'}. h('Standard');
1057             $c .= ($req{$t}{'header_filter_hits'} == 1) ? " time, " : " times, ";
1058
1059             if ($req{$t}{'old_header_size'} !=  $req{$t}{'new_header_size'}) {
1060
1061                 $c .= "changing size from ";
1062                 $c .=  h('Number') . $req{$t}{'old_header_size'} . h('Standard');
1063                 $c .= " to ";
1064                 $c .= h('Number') . $req{$t}{'new_header_size'} . h('Standard');
1065                 $c .= ".";
1066
1067             } else {
1068
1069                 $c .= "keeping the size at " . $req{$t}{'old_header_size'};
1070
1071             }
1072
1073             # Highlight from last line (XXX: What?)
1074             # $c =~ s@(?<=produced )(\d+)@$h{'Number'}$1$h{'Standard'}@;   
1075             # $c =~ s@($req{$t}{'header_filter_name'})@$h{'filter'}$1$h{'Standard'}@;
1076
1077         } else {
1078
1079            # XXX: Untested
1080            $c =~ s@(?<=produced )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1081            $c =~ s@(?<=new size )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1082
1083         }
1084         $content = $c;
1085
1086     } elsif ($c =~ m/^(Tagger|Filter) ([^\s]*) has empty joblist. Nothing to do./) {
1087
1088         # Filter privoxy-filter-test has empty joblist. Nothing to do.
1089         # Tagger variable-test has empty joblist. Nothing to do.
1090
1091         $content =~ s@(?<=$1 )([^\s]*)@$h{'filter'}$1$h{'Standard'}@;
1092
1093     } elsif ($c =~ m/^(?:re_)?filtering ([^\s]+) \(size (\d+)\) with (?:filter )?\'?([^\s]+?)\'? produced (\d+) hits \(new size (\d+)\)/) {
1094
1095         # XXX: only the second version gets highlighted properly.
1096         # re_filtering www.lfk.de/favicon.ico (size 209) with filter untrackable-hulk produced 0 hits (new size 209).
1097         # filtering aci.blogg.de/ (size 37988) with 'blogg.de' produced 3 hits (new size 38057)
1098         $req{$t}{'content_source'} = $1;
1099         $req{$t}{'content_size'}   = $2;
1100         $req{$t}{'content_filter'} = $3;
1101         $req{$t}{'content_hits'}   = $4;
1102         $req{$t}{'new_content_size'} = $5;
1103         $req{$t}{'content_size_change'} = $req{$t}{'new_content_size'} - $req{$t}{'content_size'};
1104         #return '' if ($req{$t}{'content_hits'} == 0 && !cli_option_is_set('show-ineffective-filters'));
1105         if ($req{$t}{'content_hits'} == 0 and
1106             not (cli_option_is_set('show-ineffective-filters')
1107                  or ($req{$t}{'content_filter'} =~ m/^privoxy-filter-test$/))) {
1108                 return ''; 
1109         }
1110
1111         $c =~ s@(?<=\(size )(\d+)\)(?= with)@$h{'Number'}$1$h{'Standard'}@;
1112         $c =~ s@(?<=\(new size )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1113         $c =~ s@(?<=produced )(\d+)(?= hits)@$h{'Number'}$1$h{'Standard'}@;
1114
1115         $c =~ s@([^\s]+?)(\'? produced)@$h{'filter'}$1$h{'Standard'}$2@;
1116         $c = highlight_matched_host($c, '(?<=filtering )[^\s]+');
1117
1118         $c =~ s@\.$@ @;
1119         $c .= "(" . $h{'Number'};
1120         $c .= "+" if ($req{$t}{'content_size_change'} >= 0);
1121         $c .= $req{$t}{'content_size_change'} . $h{'Standard'} . ")";
1122         $content = $c;
1123
1124     } elsif ($c =~ m/^De-chunking successful. Shrunk from (\d+) to (\d+)/) {
1125
1126         $req{$t}{'chunked-size'} = $1;
1127         $req{$t}{'dechunked-size'} = $2;
1128         $req{$t}{'dechunk-change'} = $req{$t}{'dechunked-size'} - $req{$t}{'chunked-size'};
1129
1130         $content .= " (" . h('Number') . $req{$t}{'dechunk-change'} . h('Standard') . ")";
1131
1132         $content =~ s@(?<=from )($req{$t}{'chunked-size'})@$h{'Number'}$1$h{'Standard'}@;
1133         $content =~ s@(?<=to )($req{$t}{'dechunked-size'})@$h{'Number'}$1$h{'Standard'}@;
1134
1135     } elsif ($c =~ m/^Decompression successful. Old size: (\d+), new size: (\d+)./) {
1136
1137         # Decompression successful. Old size: 670, new size: 1166.
1138
1139         $req{$t}{'size-compressed'} = $1;
1140         $req{$t}{'size-decompressed'} = $2;
1141         $req{$t}{'decompression-gain'} = $req{$t}{'size-decompressed'} - $req{$t}{'size-compressed'};
1142
1143         $content =~ s@(?<=Old size: )($req{$t}{'size-compressed'})@$h{'Number'}$1$h{'Standard'}@;
1144         $content =~ s@(?<=new size: )($req{$t}{'size-decompressed'})@$h{'Number'}$1$h{'Standard'}@;
1145
1146         # XXX: Create sub get_percentage()
1147         if ($req{$t}{'size-decompressed'}) {
1148             $req{$t}{'decompression-gain-percent'} =
1149                 $req{$t}{'decompression-gain'} / $req{$t}{'size-decompressed'} * 100;
1150
1151             $content .= " (saved: ";
1152             #$content .= h('Number') . $req{$t}{'decompression-gain'} . h('Standard');
1153             #$content .= "/";
1154             $content .= h('Number') . sprintf("%.2f%%", $req{$t}{'decompression-gain-percent'}) . h('Standard');
1155             $content .= ")";
1156         }
1157
1158     } elsif ($c =~ m/^(Need to de-chunk first)/) {
1159
1160         # Need to de-chunk first
1161         return '' if SUPPRESS_NEED_TO_DE_CHUNK_FIRST;
1162
1163     } elsif ($c =~ m/^(Adding (?:dynamic )?re_filter job)/) {
1164
1165         return ''  if (SUPPRESS_SUCCEEDED_FILTER_ADDITIONS && m/succeeded/);
1166
1167         # Adding re_filter job ...
1168         # Adding dynamic re_filter job s@^(?:\w*)\s+.*\s+HTTP/\d\.\d\s*@IP-ADDRESS: $origin@D\
1169         #  to filter client-ip-address succeeded.
1170
1171     } elsif ($c =~ m/^Reading in filter/) {
1172
1173         return '' unless SHOW_FILTER_READIN_IN;
1174
1175     } else {
1176
1177         found_unknown_content($content);
1178
1179     }
1180
1181     return $content;
1182 }
1183
1184 sub handle_loglevel_redirect ($) {
1185
1186     my $c = shift;
1187     our $t;
1188     our %req;
1189     our %h;
1190
1191     if ($c =~ m/^Decoding "([^""]*)"/) {
1192
1193          $req{$t}{'original-destination'} = $1;
1194          $c = highlight_matched_path($c, '(?<=Decoding ")[^"]*');
1195          $c =~ s@\"@@g;
1196
1197     } elsif ($c =~ m/^Checking/) {
1198
1199          # Checking /_ylt=A0geu.Z76BRGR9k/**http://search.yahoo.com/search?p=view+odb+presentation+on+freebsd\
1200          #  &ei=UTF-8&xargs=0&pstart=1&fr=moz2&b=11 for redirects.
1201
1202          # TODO: Change colour if really url-decoded
1203          $req{$t}{'decoded-original-destination'} = $1;
1204          $c = highlight_matched_path($c, '(?<=Checking ")[^"]*');
1205          $c =~ s@\"@@g;
1206
1207     } elsif ($c =~ m/^pcrs command "([^""]*)" changed "([^""]*)" to "([^""]*)" \((\d+) hits?\)/) {
1208
1209         # pcrs command "s@&from=rss@@" changed "http://it.slashdot.org/article.pl?sid=07/03/02/1657247&from=rss"\
1210         #  to "http://it.slashdot.org/article.pl?sid=07/03/02/1657247" (1 hit).
1211
1212         my ($pcrs_command, $url_before, $url_after, $hits) = ($1, $2, $3, $4); # XXX: save these?
1213
1214         $c =~ s@(?<=pcrs command )"([^""]*)"@$h{'filter'}$1$h{'Standard'}@;
1215         $c = highlight_matched_url($c, '(?<=changed ")[^""]*');
1216         $c =~ s@(?<=changed )"([^""]*)"@$1@; # Remove quotes
1217         $c = highlight_matched_url($c, '(?<=to ")[^""]*');
1218         $c =~ s@(?<=to )"([^""]*)"@$1@; # Remove quotes
1219         $c =~ s@(\d+)(?= hits?)@$h{'hits'}$1$h{'Standard'}@;
1220
1221     } elsif ($c =~ m/(^New URL is: )(.*)/) {
1222
1223         # New URL is: http://it.slashdot.org/article.pl?sid=07/03/04/1511210
1224         # XXX: Use URL highlighter
1225         # XXX: Save?
1226         $c = $1 . h('rewritten-URL') . $2 . h('Standard');
1227
1228     } elsif ($c =~ m/No pcrs command recognized, assuming that/) {
1229         # No pcrs command recognized, assuming that "http://config.privoxy.org/user-manual/favicon.png"\
1230         #  is already properly formatted.
1231         # XXX: assume the same?
1232         $c = highlight_matched_url($c, '(?<=assuming that \")[^"]*');
1233
1234     } else {
1235
1236         found_unknown_content($c);
1237
1238     }
1239
1240     return $c;
1241 }
1242
1243 sub handle_loglevel_gif_deanimate ($) {
1244
1245     my $content = shift;
1246     our $t;
1247     our %req;
1248     our %h;
1249
1250     if ($content =~ m/Success! GIF shrunk from (\d+) bytes to (\d+)\./) {
1251
1252         my $bytes_from = $1;
1253         my $bytes_to = $2;
1254         # Gif-Deanimate: Success! GIF shrunk from 205 bytes to 133.
1255         $content =~ s@$bytes_from@$h{'Number'}$bytes_from$h{'Standard'}@;
1256         # XXX: Do we need g in case of ($1 == $2)?
1257         $content =~ s@$bytes_to@$h{'Number'}$bytes_to$h{'Standard'}@;
1258
1259     } elsif ($content =~ m/GIF (not) changed/) {
1260
1261         # Gif-Deanimate: GIF not changed.
1262         return '' if SUPPRESS_GIF_NOT_CHANGED;
1263         $content =~ s@($1)@$h{'not'}$1$h{'Standard'}@;
1264
1265     } elsif ($content =~ m/^failed! \(gif parsing\)/) {
1266
1267         # failed! (gif parsing)
1268         # XXX: Replace this error message with something less stupid 
1269         $content =~ s@(failed!)@$h{'error'}$1$h{'Standard'}@;
1270
1271     } elsif ($content =~ m/^Need to de-chunk first/) {
1272
1273         # Need to de-chunk first
1274         return '' if SUPPRESS_NEED_TO_DE_CHUNK_FIRST;
1275
1276     } elsif ($content =~ m/^(?:No GIF header found|failed while parsing)/) {
1277
1278         # No GIF header found (XXX: Did I ever commit this?)
1279         # failed while parsing 195 134747048 (XXX: never commited)
1280
1281         # Ignore these for now
1282
1283     } else {
1284
1285         found_unknown_content($content);
1286
1287     }
1288
1289     return $content;
1290 }
1291
1292 sub handle_loglevel_request ($) {
1293
1294     my $content = shift;
1295     our $t;
1296     our %req;
1297     our %h;
1298     our %reason_colours;
1299
1300     if ($content =~ m/crunch! /) {
1301
1302         # config.privoxy.org/send-stylesheet crunch! (CGI Call)
1303
1304         # Highlight crunch reasons
1305         foreach my $reason (keys %reason_colours) {
1306             $content =~ s@\(($reason)\)@$reason_colours{$reason}($1)$h{'Standard'}@g;
1307         }
1308         # Highlight request URL domain and ditch 'crunch!'
1309         $content = highlight_matched_pattern($content, 'request_', '[^ ]*(?= crunch!)');
1310         $content =~ s@ crunch!@@;
1311
1312     } elsif ($content =~ m/\[too long, truncated\]$/) {
1313
1314         # config.privoxy.org/edit-actions-submit?f=3&v=1176116716&s=7&Submit=Submit[...]&filter... [too long, truncated]
1315         $content = highlight_matched_pattern($content, 'request_', '^.*(?=\.\.\. \[too long, truncated\]$)');
1316
1317     } elsif ($content =~ m/(.*)/) { # XXX: Pretty stupid
1318
1319         # trac.vidalia-project.net/wiki/Volunteer?format=txt
1320         $content = h('request_') . $content . h('Standard');
1321
1322     } else {  # XXX: Nop
1323
1324         found_unknown_content($content);
1325
1326     }
1327             
1328     return $content;
1329 }
1330
1331 sub handle_loglevel_crunch ($) {
1332
1333     my $content = shift;
1334     our %h;
1335     our %reason_colours;
1336
1337     # Blocked: ads.example.org/
1338
1339     # Highlight crunch reason
1340     foreach my $reason (keys %reason_colours) {
1341         $content =~ s@($reason)@$reason_colours{$reason}$1$h{'Standard'}@g;
1342     }
1343     # Highlight request URL
1344     $content = highlight_matched_pattern($content, 'request_', '(?<= )[^ \[]*$');
1345
1346     if ($content =~ m/\[too long, truncated\]$/) {
1347
1348         # Blocked: config.privoxy.org/edit-actions-submit?f=3&v=1176116716&s=7&Submit=Submit\
1349         #  [...]&filter... [too long, truncated]
1350         $content = highlight_matched_pattern($content, 'request_', '^.*(?=\.\.\. \[too long, truncated\]$)');
1351
1352     }
1353
1354     return $content;
1355 }
1356
1357 sub handle_loglevel_connect ($) {
1358
1359     my $c = shift;
1360     our $t;
1361     our %req;
1362     our %h;
1363
1364     if ($c =~ m/^via [^\s]+ to: [^\s]+/) {
1365
1366         # Connect: via 10.0.0.1:8123 to: www.example.org.noconnect
1367
1368         $c = highlight_matched_host($c, '(?<=via )[^\s]+');
1369         $c = highlight_matched_host($c, '(?<=to: )[^\s]+');
1370
1371     } elsif ($c =~ m/^connect to: .* failed: .*/) {
1372
1373         # connect to: www.example.org.noconnect failed: Operation not permitted
1374
1375         $c = highlight_matched_host($c, '(?<=connect to: )[^\s]+');
1376
1377         $c =~ s@(?<=failed: )(.*)@$h{'error'}$1$h{'Standard'}@;
1378
1379     } elsif ($c =~ m/^to ([^\s]*) successful$/) {
1380
1381         # Connect: to www.nzherald.co.nz successful
1382
1383         return '' if SUPPRESS_SUCCESSFUL_CONNECTIONS;
1384         $c = highlight_matched_host($c, '(?<=to )[^\s]+');
1385
1386     } elsif ($c =~ m/^to ([^\s]*)$/) {
1387
1388         # Connect: to lists.sourceforge.net:443
1389
1390         $c = highlight_matched_host($c, '(?<=to )[^\s]+');
1391
1392     } elsif ($c =~ m/^accepted connection from .*/ or
1393              $c =~ m/^OK/) {
1394
1395         # accepted connection from 10.0.0.1
1396         # Privoxy 3.0.6 and earlier just say:
1397         # OK
1398         return '' if SUPPRESS_ACCEPTED_CONNECTIONS;
1399         $c = highlight_matched_host($c, '(?<=connection from ).*');
1400
1401     } elsif ($c =~ m/^write header to: .* failed:/) {
1402
1403         # write header to: 10.0.0.1 failed: Broken pipe
1404
1405         $c = highlight_matched_host($c, '(?<=write header to: )[^\s]*');
1406         $c =~ s@(?<=failed: )(.*)@$h{'Error'}$1$h{'Standard'}@;
1407
1408     } elsif ($c =~ m/^write header to client failed:/) {
1409
1410         # write header to client failed: Broken pipe
1411         # XXX: Stil in use?
1412         $c =~ s@(?<=failed: )(.*)@$h{'Error'}$1$h{'Standard'}@;
1413
1414     } elsif ($c =~ m/^socks4_connect:/) {
1415
1416         # socks4_connect: SOCKS request rejected or failed.
1417         $c =~ s@(?<=socks4_connect: )(.*)@$h{'Error'}$1$h{'Standard'}@;
1418
1419     } elsif ($c =~ m/^Listening for new connections/ or
1420              $c =~ m/^accept connection/) {
1421         # XXX: Highlight?
1422         # Privoxy versions above 3.0.6 say:
1423         # Listening for new connections ...
1424         # earlier versions say:
1425         # accept connection ...
1426         return '';
1427
1428     } elsif ($c =~ m/^accept failed:/) {
1429
1430         $c =~ s@(?<=accept failed: )(.*)@$h{'Error'}$1$h{'Standard'}@;
1431
1432     } elsif ($c =~ m/^Overriding forwarding settings/) {
1433
1434         # Overriding forwarding settings based on 'forward 10.0.0.1:8123'
1435         $c =~ s@(?<=based on \')(.*)(?=\')@$h{'configuration-line'}$1$h{'Standard'}@;
1436
1437     } elsif ($c =~ m/^Denying suspicious CONNECT request from/) {
1438
1439         # Denying suspicious CONNECT request from 10.0.0.1
1440         $c = highlight_matched_host($c, '(?<=from )[^\s]+'); # XXX: not an URL
1441
1442     } elsif ($c =~ m/^socks5_connect:/) {
1443     
1444         $c =~ s@(?<=socks5_connect: )(.*)@$h{'error'}$1$h{'Standard'}@;
1445
1446     } elsif ($c =~ m/^Created new connection to/) {
1447
1448         # Created new connection to www.privoxy.org:80 on socket 11.
1449         $c = highlight_matched_host($c, '(?<=connection to )[^\s]+');
1450         $c =~ s@(?<=on socket )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1451
1452     } elsif ($c =~ m/^^Found reusable socket/) {
1453
1454         # Found reusable socket 9 for www.privoxy.org:80 in slot 0.
1455         $c =~ s@(?<=Found reusable socket )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1456         $c = highlight_matched_host($c, '(?<=for )[^\s]+');
1457         $c =~ s@(?<=in slot )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1458
1459     } elsif ($c =~ m/^Marking open socket/) {
1460
1461         # Marking open socket 9 for www.privoxy.org:80 in slot 0 as unused.
1462         $c =~ s@(?<=Marking open socket )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1463         $c = highlight_matched_host($c, '(?<=for )[^\s]+');
1464         $c =~ s@(?<=in slot )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1465
1466     } elsif ($c =~ m/^No reusable/) {
1467
1468         # No reusable socket for addons.mozilla.org:443 found. Opening a new one.
1469         $c = highlight_matched_host($c, '(?<=for )[^\s]+');
1470
1471     } elsif ($c =~ m/^(Remembering|Forgetting) socket/) {
1472
1473         # Remembering socket 13 for www.privoxy.org:80 in slot 0.
1474         # Forgetting socket 38 for www.privoxy.org:80 in slot 5.
1475         $c =~ s@(?<=socket )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1476         $c = highlight_matched_host($c, '(?<=for )[^\s]+');
1477         $c =~ s@(?<=in slot )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1478
1479     } elsif ($c =~ m/^Socket/) {
1480
1481         # Socket 16 already forgotten or never remembered.
1482         $c =~ s@(?<=Socket )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1483
1484     } elsif ($c =~ m/^The connection to/) {
1485
1486         # The connection to www.privoxy.org:80 in slot 6 timed out. Closing socket 19.
1487         # The connection to 10.0.0.1:80 in slot 0 is no longer usable. Closing socket 4.
1488         $c = highlight_matched_host($c, '(?<=connection to )[^\s]+');
1489         $c =~ s@(?<=in slot )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1490         $c =~ s@(?<=Closing socket )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1491
1492     } elsif ($c =~ m/^Initialized/) {
1493
1494         # Initialized 20 socket slots.
1495         $c =~ s@(?<=Initialized )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1496
1497     } elsif ($c =~ m/^Done reading from server/) {
1498
1499         # Done reading from server. Expected content length: 24892. \
1500         #  Actual content length: 24892. Most recently received: 4412.
1501         $c =~ s@(?<=Expected content length: )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1502         $c =~ s@(?<=Actual content length: )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1503         $c =~ s@(?<=received: )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1504
1505     } elsif ($c =~ m/^Continuing buffering headers/) {
1506
1507         # Continuing buffering headers. byte_count: 19. header_offset: 517. len: 536.
1508         $c =~ s@(?<=byte_count: )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1509         $c =~ s@(?<=header_offset: )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1510         $c =~ s@(?<=len: )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1511
1512     } elsif ($c =~ m/^Received \d+ bytes while/) {
1513
1514         # Received 206 bytes while expecting 12103.
1515         $c =~ s@(?<=Received )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1516         $c =~ s@(?<=expecting )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1517
1518     } elsif ($c =~ m/^Looks like we rea/ or
1519              $c =~ m/^Unsetting keep-alive flag/) {
1520
1521         # Looks like we reached the end of the last chunk. We better stop reading.
1522         # Looks like we read the end of the last chunk together with the server \
1523         #  headers. We better stop reading.
1524         # Unsetting keep-alive flag.
1525
1526     } else {
1527
1528         found_unknown_content($c);
1529
1530     }
1531             
1532     return $c;
1533 }
1534
1535
1536 sub handle_loglevel_info ($) {
1537
1538     my $c = shift;
1539     our $t;
1540     our %req;
1541     our %h;
1542  
1543     if ($c =~ m/^Rewrite detected:/) {
1544
1545         # Rewrite detected: GET http://10.0.0.2:88/blah.txt HTTP/1.1
1546         $c = highlight_matched_request_line($c, '(?<=^Rewrite detected: ).*');
1547
1548     } elsif ($c =~ m/^Decompress(ing deflated|ion didn)/ or
1549              $c =~ m/^Compressed content detected/ or
1550              $c =~ m/^Tagger/
1551             ) {
1552         # Decompressing deflated iob: 117
1553         # Decompression didn't result in any content.
1554         # Compressed content detected, content filtering disabled. Consider recompiling Privoxy\
1555         #  with zlib support or enable the prevent-compression action.
1556         # Tagger 'complete-url' created empty tag. Ignored.
1557
1558         # Ignored for now
1559
1560     } elsif ($c =~ m/^(Re)?loading configuration file /) {
1561
1562         # loading configuration file '/usr/local/etc/privoxy/config':
1563         # Reloading configuration file '/usr/local/etc/privoxy/config'
1564         $c =~ s@(?<=loading configuration file \')([^\']*)@$h{'file'}$1$h{'Standard'}@;
1565
1566     } elsif ($c =~ m/^exiting by signal/) {
1567         
1568         # exiting by signal 15 .. bye
1569         $c =~ s@(?<=exiting by signal )(\d+)@$h{'signal'}$1$h{'Standard'}@;
1570
1571     } elsif ($c =~ m/^Privoxy version/) {
1572         
1573         # Privoxy version 3.0.7
1574         $c =~ s@(?<=^Privoxy version )(\d+\.\d+\.\d+)$@$h{'version'}$1$h{'Standard'}@;
1575
1576     } elsif ($c =~ m/^Program name: /) {
1577
1578         # Program name: /usr/local/sbin/privoxy
1579         $c =~ s@(?<=Program name: )(.*)@$h{'program-name'}$1$h{'Standard'}@;
1580
1581     } elsif ($c =~ m/^Listening on port /) {
1582
1583         # Listening on port 8118 on IP address 10.0.0.1
1584         $c =~ s@(?<=Listening on port )(\d+)@$h{'port'}$1$h{'Standard'}@;
1585         $c =~ s@(?<=on IP address )(.*)@$h{'ip-address'}$1$h{'Standard'}@;
1586
1587     } elsif ($c =~ m/^\(Re-\)Open(?:ing)? logfile/) {
1588
1589         # (Re-)Open logfile /var/log/privoxy/privoxy.log
1590         $c =~ s@(?<=Open logfile )(.*)@$h{'file'}$1$h{'Standard'}@;
1591
1592     } elsif ($c =~ m/^(Request from|Malformed server response detected)/) {
1593
1594         # Request from 10.0.0.1 denied. limit-connect{,} doesn't allow CONNECT requests to port 443.
1595         # Request from 10.0.0.1 marked for blocking. limit-connect{,} doesn't allow CONNECT requests to port 443.
1596         # Malformed server response detected. Downgrading to HTTP/1.0 impossible.
1597
1598         $c =~ s@(?<=Request from )([^\s]*)@$h{'ip-address'}$1$h{'Standard'}@;
1599         $c =~ s@(denied|blocking)@$h{'warning'}$1$h{'Standard'}@;
1600         $c =~ s@(CONNECT)@$h{'method'}$1$h{'Standard'}@;
1601         $c =~ s@(?<=to port )(\d+)@$h{'port'}$1$h{'Standard'}@;
1602
1603     } elsif ($c =~ m/^Status code/) {
1604
1605         # Status code 304 implies no body.
1606         $c =~ s@(?<=Status code )(\d+)@$h{'status-code'}$1$h{'Standard'}@;
1607
1608     } elsif ($c =~ m/^Method/) {
1609
1610         # Method HEAD implies no body.
1611         $c =~ s@(?<=Method )([^\s]+)@$h{'method'}$1$h{'Standard'}@;
1612
1613     } elsif ($c =~ m/^No logfile configured/ or
1614              $c =~ m/^Malformerd HTTP headers detected and MS IIS5 hack enabled/ or
1615              $c =~ m/^Invalid \"chunked\" transfer/ or
1616              $c =~ m/^Support for/
1617              ) {
1618
1619         # No logfile configured. Please enable it before reporting any problems.
1620         # Malformerd HTTP headers detected and MS IIS5 hack enabled. Expect an invalid \
1621         #  response or even no response at all.
1622         # No logfile configured. Logging disabled.
1623         # Invalid "chunked" transfer encoding detected and ignored.
1624         # Support for 'Connection: keep-alive' is experimental, incomplete and\
1625         #  known not to work properly in some situations.
1626
1627     } else {
1628
1629         found_unknown_content($c);
1630
1631     }
1632
1633     return $c;
1634 }
1635
1636 sub handle_loglevel_cgi ($) {
1637
1638     my $c = shift;
1639     our $t;
1640     our %req;
1641     our %h;
1642
1643     if ($c =~ m/^Granting access to/) {
1644       
1645         #Granting access to http://config.privoxy.org/send-stylesheet, referrer http://p.p/ is trustworthy.
1646
1647     } elsif ($c =~ m/^Substituting: s(.)/) {
1648       
1649         # Substituting: s/@else-not-FEATURE_ZLIB@.*@endif-FEATURE_ZLIB@//sigTU
1650         # XXX: prone to span several lines
1651
1652         my $delimiter = $1;
1653         #$c =~ s@(?<=failed: )(.*)@$h{'error'}$1$h{'Standard'}@;
1654         $c =~ s@(?!<=\\)($delimiter)@$h{'pcrs-delimiter'}$1$h{'Standard'}@g; # XXX: Too aggressive
1655         #$c =~ s@(?!<=\\)($1)@$h{'pcrs-delimiter'}$1$h{'Standard'}@g;
1656     }
1657
1658     return $c;
1659 }
1660
1661 sub handle_loglevel_force ($) {
1662
1663     my $c = shift;
1664     our $t;
1665     our %req;
1666     our %h;
1667
1668     if ($c =~ m/^Ignored force prefix in request:/) {
1669       
1670         # Ignored force prefix in request: "GET http://10.0.0.1/PRIVOXY-FORCE/block HTTP/1.1"
1671         $c =~ s@^(Ignored)@$h{'ignored'}$1$h{'Standard'}@;
1672         $c = highlight_matched_request_line($c, '(?<=request: ")[^"]*');
1673
1674     } elsif ($c =~ m/^Enforcing request:/) {
1675       
1676         # Enforcing request: "GET http://10.0.0.1/block HTTP/1.1".
1677         $c = highlight_matched_request_line($c, '(?<=request: ")[^"]*');
1678
1679     } else {
1680
1681         found_unknown_content($c);
1682
1683     }
1684
1685     return $c;
1686 }
1687
1688 sub handle_loglevel_ignore ($) {
1689     return shift;
1690 }
1691
1692 ################################################################################
1693 # Functions that actually print stuff
1694 ################################################################################
1695
1696 sub print_clf_message () {
1697
1698     our ($ip, $timestamp, $request_line, $status_code, $size);
1699     our %h;
1700     my $output = '';
1701
1702     return if DEBUG_SUPPRESS_LOG_MESSAGES;
1703
1704     # Rebuild highlighted
1705     $output .= $h{'Number'} . $ip . $h{'Standard'};
1706     $output .= " - - ";
1707     $output .= "[" . $h{'Timestamp'} . $timestamp . $h{'Standard'} . "]";
1708     $output .= " ";
1709     $output .= "\"" . highlight_request_line("$request_line") . "\"";
1710     $output .= " ";
1711     $output .= $h{'Status'} . $status_code . $h{'Standard'};
1712     $output .= " ";
1713     $output .= $h{'Number'} . $size . $h{'Standard'};
1714     $output .= get_line_end();
1715
1716     print $output;
1717 }
1718
1719 sub print_non_clf_message ($) {
1720
1721     our %req;
1722     our %thread_colours;
1723     our %h;
1724     our $t;
1725     our $time_colour_index;
1726     our @time_colours;
1727     my $output;
1728     my $content = shift;
1729     my ($day, $time_stamp, $msecs, $thread, $log_level)
1730      = ($req{$t}{'day'}, $req{$t}{'time-stamp'}, $req{$t}{'msecs'}, $t, $req{$t}{'log-level'} );
1731
1732     return if DEBUG_SUPPRESS_LOG_MESSAGES;
1733
1734     $output .= $h{"Standard"} unless cli_option_is_set('html-output');
1735     #    $output .= "$day ";
1736     $output .= $time_colours[$time_colour_index % 2]; 
1737
1738     $output .= $time_stamp;
1739     $output .= ".$msecs" unless cli_option_is_set('no-msecs');
1740     $output .= $h{"Standard"};
1741     $output .= " ";
1742     $output .= $thread_colours{$thread} if (defined($thread_colours{$thread}));
1743     $output .= $thread;
1744     $output .= $h{"Standard"} . " ";
1745     $output .= $h{$log_level} if (defined($h{$log_level}));
1746     $output .= $log_level;
1747     $output .= $h{"Standard"} . ": ";
1748     $output .= "$content";
1749     $output .= get_line_end();
1750
1751     print $output;
1752 }
1753
1754 sub parse_loop () {
1755
1756     our $t;
1757     our %req; # request data from previous lines
1758     our %h;
1759     our %thread_colours;
1760     our @all_colours;
1761     our @time_colours;
1762     our $thread_colour_index = 0;
1763     our $header_colour_index = 0;
1764     our $time_colour_index = 0;
1765
1766     my ($day, $time_stamp, $thread, $log_level, $content, $c, $msecs);
1767     my $last_msecs  = 0;
1768     my $last_thread = 0;
1769     my $last_timestamp = 0;
1770     my $output;
1771     my $filters_that_did_nothing;
1772     my $key;
1773     my $time_colour;
1774     our $no_special_header_highlighting;
1775     $time_colour = paint_it('white');
1776     my %log_level_count;
1777
1778     my %log_level_handlers = (
1779         'Re-Filter'     => \&handle_loglevel_re_filter,
1780         'Header'        => \&handle_loglevel_header,
1781         'Connect'       => \&handle_loglevel_connect,
1782         'Redirect'      => \&handle_loglevel_redirect,
1783         'Request'       => \&handle_loglevel_request,
1784         'Crunch'        => \&handle_loglevel_crunch,
1785         'Gif-Deanimate' => \&handle_loglevel_gif_deanimate,
1786         'Info'          => \&handle_loglevel_info,
1787         'CGI'           => \&handle_loglevel_cgi,
1788         'Force'         => \&handle_loglevel_force,
1789         'Error'         => \&handle_loglevel_ignore,
1790         'Fatal error'   => \&handle_loglevel_ignore,
1791         'Writing'       => \&handle_loglevel_ignore,
1792     );
1793
1794     while (<>) {
1795  
1796         $output = '';
1797
1798         if (m/^(\w{3} \d{2}) (\d\d:\d\d:\d\d)\.?(\d+)? (?:Privoxy\()?([^\)\s]*)[\)]? ([\w -]*): (.*)$/) {
1799             # XXX: Put in req hash?
1800             $day = $1;
1801             $time_stamp = $2;
1802             $msecs = $3 ? $3 : 0; # Only the cool kids have micro second resolution
1803             $log_level = $5;
1804             $content = $c = $6;
1805             $thread = $t = $4;
1806
1807             $req{$t}{'day'} = $day;
1808             $req{$t}{'time-stamp'} = $time_stamp;
1809             $req{$t}{'msecs'} = $msecs; # Only the cool kids have micro second resolution;
1810             $req{$t}{'log-level'} = $log_level;
1811             $req{$t}{'content'} = $content;
1812             $req{$t}{'log-message'} = $_;
1813             $no_special_header_highlighting = 0;
1814
1815             $log_level_count{$log_level}++;
1816
1817             if (defined($log_level_handlers{$log_level})) {
1818
1819                 $content = $log_level_handlers{$log_level}($content);
1820
1821             } else {
1822
1823                 die "No handler found for log level \"$log_level\"\n";
1824
1825             }
1826
1827             # Highlight Truncations    
1828             if (m/\.\.\. \[(too long, truncated)/) {
1829                 $content =~ s@($1)@$h{'Truncation'}$1$h{'Standard'}@g;
1830             }
1831
1832             next unless $content;
1833
1834             # Register threads to keep the colour constant
1835             if (!defined($thread_colours{$thread})) {
1836                 $thread_colours{$thread} = $all_colours[$thread_colour_index % @all_colours];
1837                 $thread_colour_index++;
1838             }
1839
1840             # Switch timestamp colour if timestamps differ
1841             if ($msecs != $last_msecs || !($time_stamp =~ m/$last_timestamp/)) {
1842                debug_message("Tick tack!") if DEBUG_TICKS;
1843                $time_colour = $time_colours[$time_colour_index % 2]; 
1844                $time_colour_index++
1845             }
1846
1847             $last_msecs = $msecs;
1848             $last_thread = $thread;
1849             $last_timestamp = $time_stamp;
1850
1851             print_non_clf_message($content);
1852
1853         } elsif (m/^(\d+\.\d+\.\d+\.\d+) - - \[(.*)\] "(.*)" (\d+) (\d+)/) {
1854
1855             # LOG_LEVEL_CLF lines look like this
1856             # 61.152.239.32 - - [04/Mar/2007:18:28:23 +0100] "GET \
1857             #  http://ad.yieldmanager.com/imp?z=1&Z=120x600&s=109339&u=http%3A%2F%2Fwww.365loan.co.uk%2F&r=1\
1858             #  HTTP/1.1" 403 1730
1859             our ($ip, $timestamp, $request_line, $status_code, $size) = ($1, $2, $3, $4, $5);
1860
1861             print_clf_message();
1862     
1863         } else {
1864
1865             # Some Privoxy log messages span more than one line,
1866             # usually to dump lots of content that doesn't need any syntax highlighting.
1867             # XXX: add mechanism to forward these lines to the right handler anyway.
1868             chomp();
1869             unless (DEBUG_SUPPRESS_LOG_MESSAGES or (SUPPRESS_EMPTY_LINES and m/^\s+$/)) {
1870                 print and print get_line_end(); # unless (SUPPRESS_EMPTY_LINES and m/^\s+$/);
1871             }
1872         }
1873     }
1874
1875     if (cli_option_is_set('statistic')) {
1876         foreach (keys %log_level_count) {
1877             print $_ . ": " . $log_level_count{$_} . " ";
1878         }
1879     }
1880 }
1881
1882 sub VersionMessage {
1883     my $version_message;
1884
1885     $version_message .= 'Privoxy-Log-Parser ' . PRIVOXY_LOG_PARSER_VERSION  . "\n";
1886     $version_message .= 'Copyright (C) 2007-2008 Fabian Keil <fk@fabiankeil.de>' . "\n";
1887     $version_message .= 'http://www.fabiankeil.de/sourcecode/privoxy-log-parser/' . "\n";
1888
1889     print $version_message;
1890 }
1891
1892 sub get_cli_options () {
1893
1894     our %cli_options = (
1895         'html-output'              => CLI_OPTION_DEFAULT_TO_HTML_OUTPUT,
1896         'title'                    => CLI_OPTION_TITLE,
1897         'no-syntax-highlighting'   => CLI_OPTION_NO_SYNTAX_HIGHLIGHTING,
1898         'no-embedded-css'          => CLI_OPTION_NO_EMBEDDED_CSS,
1899         'no-msecs'                 => CLI_OPTION_NO_MSECS,
1900         'show-ineffective-filters' => CLI_OPTION_SHOW_INEFFECTIVE_FILTERS,
1901         'accept-unknown-messages'  => CLI_OPTION_ACCEPT_UNKNOWN_MESSAGES,
1902         'statistic'                => CLI_OPTION_STATISTIC,
1903     ); 
1904
1905     GetOptions (
1906         'html-output'              => \$cli_options{'html-output'},
1907         'title'                    => \$cli_options{'title'},
1908         'no-syntax-highlighting'   => \$cli_options{'no-syntax-highlighting'},
1909         'no-embedded-css'          => \$cli_options{'no-embedded-css'},
1910         'no-msecs'                 => \$cli_options{'no-msecs'},
1911         'show-ineffective-filters' => \$cli_options{'show-ineffective-filters'},
1912         'accept-unknown-messages'  => \$cli_options{'accept-unknown-messages'},
1913         'statistic'                => \$cli_options{'statistic'},
1914         'version'                  => sub { VersionMessage && exit(0) }
1915    );
1916 }
1917
1918
1919
1920 ################################################################################
1921 # main
1922 ################################################################################
1923 sub main () {
1924
1925     get_cli_options();
1926     set_background(DEFAULT_BACKGROUND);
1927     prepare_our_stuff();
1928
1929     print_intro();
1930
1931     parse_loop();
1932
1933     print_outro();
1934 }
1935
1936 main();
1937
1938 =head1 NAME
1939
1940 B<privoxy-log-parser> - A parser and syntax-highlighter for Privoxy log messages
1941
1942 =head1 SYNOPSIS
1943
1944 B<privoxy-log-parser> [B<--accept-unknown-messages>] [B<--html-output>]
1945 [B<--no-msecs>] [B<--no-syntax-higlighting>] [B<--show-ineffective-filters>]
1946 [B<--version>]
1947
1948 =head1 DESCRIPTION
1949
1950 B<privoxy-log-parser> reads Privoxy log messages and
1951
1952 - syntax-highlights recognized lines,
1953
1954 - reformats some of them for easier comprehension,
1955
1956 - filters out less useful messages, and
1957
1958 - (in some cases) calculates additional information,
1959   like the compression ratio or how a filter affected
1960   the content size.
1961  
1962 With B<privoxy-log-parser> you should be able to increase Privoxy's log level
1963 without getting confused by the resulting amount of output. For example for
1964 "debug 64" B<privoxy-log-parser> will (by default) only show messages that
1965 affect the content. If a filter doesn't cause any hits, B<privoxy-log-parser>
1966 will hide the "filter foo caused 0 hits" message.
1967
1968 =head1 OPTIONS
1969
1970 [B<--accept-unknown-messages>] Don't print warnings in case of unknown messages,
1971 just don't highlight them.
1972
1973 [B<--html-output>] Use HTML and CSS for the syntax highlighting. If this option is
1974 omitted, ANSI escape sequences are used unless B<--no-syntax-highlighting> is active.
1975 This option is only intended to make embedding log excerpts in web pages easier.
1976 It does not excape any input!
1977
1978 [B<--no-msecs>] Don't expect milisecond resolution
1979
1980 [B<--no-syntax-highlighting>] Disable syntax-highlighting. Useful when
1981 the filtered output is piped into less in which case the ANSI control
1982 codes don't work, or if the terminal itself doesn't support the control
1983 codes.
1984
1985 [B<--show-ineffective-filters>] Don't suppress log lines for filters
1986 that didn't modify the content.
1987
1988 [B<--version>] Print version and exit.
1989
1990 =head1 EXAMPLES
1991
1992 To monitor a log file:
1993
1994 tail -F /usr/jails/privoxy-jail/var/log/privoxy/privoxy.log | B<privoxy-log-parser>
1995
1996 Replace '-F' with '-f' if your tail implementation lacks '-F' support
1997 or if the log won't get rotated anyway. The log file location depends
1998 on your system (Doh!).
1999
2000 To monitor Privoxy without having it write to a log file:
2001
2002 privoxy --no-daemon /usr/jails/privoxy-jail/usr/local/etc/privoxy/config 2>&1 | B<privoxy-log-parser>
2003
2004 Again, the config file location depends on your system. Output redirection
2005 depends on your shell, the above works with bourne shells.
2006
2007 To read a processed Privoxy log file from top to bottom, letting the content
2008 scroll by slightly faster than you can read:
2009
2010 B<privoxy-log-parser> < /usr/jails/privoxy-jail/var/log/privoxy/privoxy.log
2011
2012 This is probably only useful to fill screens in the background of haxor movies.
2013
2014 =head1 CAVEATS
2015
2016 Syntax highlighting with ANSI escape sequences will look strange
2017 if your background color isn't black.
2018
2019 Some messages aren't recognized yet and will not be fully highlighted.
2020
2021 B<privoxy-log-parser> is developed with Privoxy 3.0.7 or later in mind,
2022 using earlier Privoxy versions will probably result in an increased amount
2023 of unrecognized log lines.
2024
2025 Privoxy's log files tend to be rather large. If you use HTML
2026 highlighting some browsers can't handle them, get confused and
2027 will eventually crash because of segmentation faults or unexpected
2028 exceptions. This is a problem in the browser and not B<privoxy-log-parser>'s
2029 fault.
2030
2031 =head1 BUGS
2032
2033 Many settings can't be controlled through command line options yet.
2034
2035 =head1 SEE ALSO
2036
2037 privoxy(1)
2038
2039 =head1 AUTHOR
2040
2041 Fabian Keil <fk@fabiankeil.de>
2042
2043 =cut