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