Highlight the assumed latency, too.
[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.46 2009/09/10 15:02:25 fabiankeil 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-2009 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.5',
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' . ' {';
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}) and $header =~ /^[\d\w-]*$/) {
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           or $c =~ m/Keeping the (?:server|client) header /
916           or $c =~ m/Content modified with no Content-Length header set/
917           or $c =~ m/^Appended client IP address to/
918           or $c =~ m/^Removing 'Connection: close' to imply keep-alive./
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         # Keeping the server header 'Connection: keep-alive' around.
958         # Keeping the client header 'Connection: close' around. The connection will not be kept alive.
959         # Keeping the client header 'Connection: keep-alive' around. The connection will be kept alive if possible.
960         # Content modified with no Content-Length header set. Creating a fake one for adjustment later on.
961         # Appended client IP address to X-Forwarded-For: 10.0.0.2, 10.0.0.1
962         # Removing 'Connection: close' to imply keep-alive.
963
964     } elsif ($c =~ m/^scanning headers for:/) {
965
966         return '' unless SHOW_SCAN_INTRO;
967
968     } elsif ($c =~ m/^[Cc]runch(ing|ed)|crumble crunched:/) {
969         # crunched User-Agent!
970         # Crunching: Content-Encoding: gzip
971
972         $content =~ s@(Crunching|crunched)@$h{$1}$1$h{'Standard'}@;
973
974     } elsif ($c =~ m/^Offending request data with NULL bytes turned into \'°\' characters:/) {
975         
976         # Offending request data with NULL bytes turned into '°' characters: Â°Â°n°°(°°°
977
978         $content = h('warning') . $content . h('Standard');
979  
980     } elsif ($c =~ m/^(Transforming \")(.*?)(\" to \")(.*?)(\")/) {
981
982         # Transforming "Proxy-Authenticate: Basic realm="Correos Proxy Server"" to\
983         #  "Proxy-Authenticate: Basic realm="Correos Proxy Server""
984
985        $content =~ s@(?<=^Transforming \")(.*)(?=\" to)@$h{'Header'}$1$h{'Standard'}@;
986        $content =~ s@(?<=to \")(.*)(?=\")@$h{'Header'}$1$h{'Standard'}@;
987
988     } elsif ($c =~ m/^Removing empty header/) {
989
990         # Removing empty header
991         # Ignore for now
992
993     } elsif ($c =~ m/^Content-Type: .* not replaced/) {
994
995         # Content-Type: application/octet-stream not replaced. It doesn't look like text.\
996         #  Enable force-text-mode if you know what you're doing.
997         # XXX: Could highlight more here.
998         $content =~ s@(?<=^Content-Type: )(.*)(?= not replaced)@$h{'content-type'}$1$h{'Standard'}@;
999
1000     } elsif ($c =~ m/^(Server|Client) keep-alive timeout is/) {
1001
1002        # Server keep-alive timeout is 5. Sticking with 10.
1003        # Client keep-alive timeout is 20. Sticking with 10.
1004
1005        $content =~ s@(?<=timeout is )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1006        $content =~ s@(?<=Sticking with )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1007
1008     } elsif ($c =~ m/^Reducing keep-alive timeout/) {
1009
1010        # Reducing keep-alive timeout from 60 to 10.
1011
1012        $content =~ s@(?<= from )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1013        $content =~ s@(?<= to )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1014
1015     } else {
1016
1017         found_unknown_content($content);
1018     }
1019
1020     # Highlight headers   
1021     unless ($c =~ m/^Transforming/) {
1022         $content = highlight_known_headers($content) unless $no_special_header_highlighting;
1023     }
1024
1025     return $content;
1026 }
1027
1028 sub handle_loglevel_re_filter ($) {
1029
1030     my $content = shift;
1031     my $c = $content;
1032     my $key;
1033     our $t;
1034     our %req;
1035     our %h;
1036     our %header_colours;
1037     our @all_colours;
1038     our $header_colour_index;
1039
1040     if ($c =~ /\.{3}$/
1041         and $c =~ m/^(?:re_)?filtering \'?(.*?)\'? \(size (\d*)\) with (?:filter )?\'?([^\s]*?)\'? ?\.{3}$/) {
1042
1043         # Used by Privoxy 3.0.5 and 3.0.6:
1044         # XXX: Fill in ...
1045         # Used by Privoxy 3.0.7:
1046         # filtering 'Connection: close' (size 17) with 'generic-content-ads' ...
1047
1048         $req{$t}{'filtered_header'} = $1;
1049         $req{$t}{'old_header_size'} = $2;
1050         $req{$t}{'header_filter_name'} = $3;
1051
1052         unless (cli_option_is_set('show-ineffective-filters') or
1053                 $req{$t}{'header_filter_name'} =~ m/^privoxy-filter-test$/) {
1054             return '';
1055         }
1056         $content =~ s@(?<=\(size )(\d+)@$h{'Number'}$1$h{'Standard'}@;   
1057         $content =~ s@($req{$t}{'header_filter_name'})@$h{'filter'}$1$h{'Standard'}@;
1058
1059     } elsif ($c =~ m/^ ?\.\.\. ?produced (\d*) hits \(new size (\d*)\)\./) {
1060
1061         # ...produced 0 hits (new size 23).
1062         #... produced 1 hits (new size 54).
1063
1064         $req{$t}{'header_filter_hits'} = $1;
1065         $req{$t}{'new_header_size'} = $2;
1066
1067         unless (cli_option_is_set('show-ineffective-filters') or
1068                 (defined($req{$t}{'header_filter_name'}) and
1069                  $req{$t}{'header_filter_name'} =~ m/^privoxy-filter-test$/)) {
1070
1071             if ($req{$t}{'header_filter_hits'} == 0 and
1072                 not (defined($req{$t}{'header_filter_name'}) and
1073                  $req{$t}{'header_filter_name'} =~ m/^privoxy-filter-test$/)) {
1074                 return ''; 
1075             }
1076             # Reformat including information from the intro
1077             $c = "'" . h('filter') . $req{$t}{'header_filter_name'} . h('Standard') . "'";
1078             $c .= " hit ";
1079             # XXX: Hide behind constant, it may be interesting if LOG_LEVEL_HEADER isn't enabled as well.
1080             # $c .= $req{$t}{'filtered_header'} . " ";
1081             $c .= h('Number') . $req{$t}{'header_filter_hits'}. h('Standard');
1082             $c .= ($req{$t}{'header_filter_hits'} == 1) ? " time, " : " times, ";
1083
1084             if ($req{$t}{'old_header_size'} !=  $req{$t}{'new_header_size'}) {
1085
1086                 $c .= "changing size from ";
1087                 $c .=  h('Number') . $req{$t}{'old_header_size'} . h('Standard');
1088                 $c .= " to ";
1089                 $c .= h('Number') . $req{$t}{'new_header_size'} . h('Standard');
1090                 $c .= ".";
1091
1092             } else {
1093
1094                 $c .= "keeping the size at " . $req{$t}{'old_header_size'};
1095
1096             }
1097
1098             # Highlight from last line (XXX: What?)
1099             # $c =~ s@(?<=produced )(\d+)@$h{'Number'}$1$h{'Standard'}@;   
1100             # $c =~ s@($req{$t}{'header_filter_name'})@$h{'filter'}$1$h{'Standard'}@;
1101
1102         } else {
1103
1104            # XXX: Untested
1105            $c =~ s@(?<=produced )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1106            $c =~ s@(?<=new size )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1107
1108         }
1109         $content = $c;
1110
1111     } elsif ($c =~ m/^(Tagger|Filter) ([^\s]*) has empty joblist. Nothing to do./) {
1112
1113         # Filter privoxy-filter-test has empty joblist. Nothing to do.
1114         # Tagger variable-test has empty joblist. Nothing to do.
1115
1116         $content =~ s@(?<=$1 )([^\s]*)@$h{'filter'}$1$h{'Standard'}@;
1117
1118     } elsif ($c =~ m/^(?:re_)?filtering ([^\s]+) \(size (\d+)\) with (?:filter )?\'?([^\s]+?)\'? produced (\d+) hits \(new size (\d+)\)/) {
1119
1120         # XXX: only the second version gets highlighted properly.
1121         # re_filtering www.lfk.de/favicon.ico (size 209) with filter untrackable-hulk produced 0 hits (new size 209).
1122         # filtering aci.blogg.de/ (size 37988) with 'blogg.de' produced 3 hits (new size 38057)
1123         $req{$t}{'content_source'} = $1;
1124         $req{$t}{'content_size'}   = $2;
1125         $req{$t}{'content_filter'} = $3;
1126         $req{$t}{'content_hits'}   = $4;
1127         $req{$t}{'new_content_size'} = $5;
1128         $req{$t}{'content_size_change'} = $req{$t}{'new_content_size'} - $req{$t}{'content_size'};
1129         #return '' if ($req{$t}{'content_hits'} == 0 && !cli_option_is_set('show-ineffective-filters'));
1130         if ($req{$t}{'content_hits'} == 0 and
1131             not (cli_option_is_set('show-ineffective-filters')
1132                  or ($req{$t}{'content_filter'} =~ m/^privoxy-filter-test$/))) {
1133                 return ''; 
1134         }
1135
1136         $c =~ s@(?<=\(size )(\d+)\)(?= with)@$h{'Number'}$1$h{'Standard'}@;
1137         $c =~ s@(?<=\(new size )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1138         $c =~ s@(?<=produced )(\d+)(?= hits)@$h{'Number'}$1$h{'Standard'}@;
1139
1140         $c =~ s@([^\s]+?)(\'? produced)@$h{'filter'}$1$h{'Standard'}$2@;
1141         $c = highlight_matched_host($c, '(?<=filtering )[^\s]+');
1142
1143         $c =~ s@\.$@ @;
1144         $c .= "(" . $h{'Number'};
1145         $c .= "+" if ($req{$t}{'content_size_change'} >= 0);
1146         $c .= $req{$t}{'content_size_change'} . $h{'Standard'} . ")";
1147         $content = $c;
1148
1149     } elsif ($c =~ m/^De-chunking successful. Shrunk from (\d+) to (\d+)/) {
1150
1151         $req{$t}{'chunked-size'} = $1;
1152         $req{$t}{'dechunked-size'} = $2;
1153         $req{$t}{'dechunk-change'} = $req{$t}{'dechunked-size'} - $req{$t}{'chunked-size'};
1154
1155         $content .= " (" . h('Number') . $req{$t}{'dechunk-change'} . h('Standard') . ")";
1156
1157         $content =~ s@(?<=from )($req{$t}{'chunked-size'})@$h{'Number'}$1$h{'Standard'}@;
1158         $content =~ s@(?<=to )($req{$t}{'dechunked-size'})@$h{'Number'}$1$h{'Standard'}@;
1159
1160     } elsif ($c =~ m/^Decompression successful. Old size: (\d+), new size: (\d+)./) {
1161
1162         # Decompression successful. Old size: 670, new size: 1166.
1163
1164         $req{$t}{'size-compressed'} = $1;
1165         $req{$t}{'size-decompressed'} = $2;
1166         $req{$t}{'decompression-gain'} = $req{$t}{'size-decompressed'} - $req{$t}{'size-compressed'};
1167
1168         $content =~ s@(?<=Old size: )($req{$t}{'size-compressed'})@$h{'Number'}$1$h{'Standard'}@;
1169         $content =~ s@(?<=new size: )($req{$t}{'size-decompressed'})@$h{'Number'}$1$h{'Standard'}@;
1170
1171         # XXX: Create sub get_percentage()
1172         if ($req{$t}{'size-decompressed'}) {
1173             $req{$t}{'decompression-gain-percent'} =
1174                 $req{$t}{'decompression-gain'} / $req{$t}{'size-decompressed'} * 100;
1175
1176             $content .= " (saved: ";
1177             #$content .= h('Number') . $req{$t}{'decompression-gain'} . h('Standard');
1178             #$content .= "/";
1179             $content .= h('Number') . sprintf("%.2f%%", $req{$t}{'decompression-gain-percent'}) . h('Standard');
1180             $content .= ")";
1181         }
1182
1183     } elsif ($c =~ m/^(Need to de-chunk first)/) {
1184
1185         # Need to de-chunk first
1186         return '' if SUPPRESS_NEED_TO_DE_CHUNK_FIRST;
1187
1188     } elsif ($c =~ m/^(Adding (?:dynamic )?re_filter job)/) {
1189
1190         return ''  if (SUPPRESS_SUCCEEDED_FILTER_ADDITIONS && m/succeeded/);
1191
1192         # Adding re_filter job ...
1193         # Adding dynamic re_filter job s@^(?:\w*)\s+.*\s+HTTP/\d\.\d\s*@IP-ADDRESS: $origin@D\
1194         #  to filter client-ip-address succeeded.
1195
1196     } elsif ($c =~ m/^Reading in filter/) {
1197
1198         return '' unless SHOW_FILTER_READIN_IN;
1199
1200     } else {
1201
1202         found_unknown_content($content);
1203
1204     }
1205
1206     return $content;
1207 }
1208
1209 sub handle_loglevel_redirect ($) {
1210
1211     my $c = shift;
1212     our $t;
1213     our %req;
1214     our %h;
1215
1216     if ($c =~ m/^Decoding "([^""]*)"/) {
1217
1218          $req{$t}{'original-destination'} = $1;
1219          $c = highlight_matched_path($c, '(?<=Decoding ")[^"]*');
1220          $c =~ s@\"@@g;
1221
1222     } elsif ($c =~ m/^Checking/) {
1223
1224          # Checking /_ylt=A0geu.Z76BRGR9k/**http://search.yahoo.com/search?p=view+odb+presentation+on+freebsd\
1225          #  &ei=UTF-8&xargs=0&pstart=1&fr=moz2&b=11 for redirects.
1226
1227          # TODO: Change colour if really url-decoded
1228          $req{$t}{'decoded-original-destination'} = $1;
1229          $c = highlight_matched_path($c, '(?<=Checking ")[^"]*');
1230          $c =~ s@\"@@g;
1231
1232     } elsif ($c =~ m/^pcrs command "([^""]*)" changed "([^""]*)" to "([^""]*)" \((\d+) hits?\)/) {
1233
1234         # pcrs command "s@&from=rss@@" changed "http://it.slashdot.org/article.pl?sid=07/03/02/1657247&from=rss"\
1235         #  to "http://it.slashdot.org/article.pl?sid=07/03/02/1657247" (1 hit).
1236
1237         my ($pcrs_command, $url_before, $url_after, $hits) = ($1, $2, $3, $4); # XXX: save these?
1238
1239         $c =~ s@(?<=pcrs command )"([^""]*)"@$h{'filter'}$1$h{'Standard'}@;
1240         $c = highlight_matched_url($c, '(?<=changed ")[^""]*');
1241         $c =~ s@(?<=changed )"([^""]*)"@$1@; # Remove quotes
1242         $c = highlight_matched_url($c, '(?<=to ")[^""]*');
1243         $c =~ s@(?<=to )"([^""]*)"@$1@; # Remove quotes
1244         $c =~ s@(\d+)(?= hits?)@$h{'hits'}$1$h{'Standard'}@;
1245
1246     } elsif ($c =~ m/(^New URL is: )(.*)/) {
1247
1248         # New URL is: http://it.slashdot.org/article.pl?sid=07/03/04/1511210
1249         # XXX: Use URL highlighter
1250         # XXX: Save?
1251         $c = $1 . h('rewritten-URL') . $2 . h('Standard');
1252
1253     } elsif ($c =~ m/No pcrs command recognized, assuming that/) {
1254         # No pcrs command recognized, assuming that "http://config.privoxy.org/user-manual/favicon.png"\
1255         #  is already properly formatted.
1256         # XXX: assume the same?
1257         $c = highlight_matched_url($c, '(?<=assuming that \")[^"]*');
1258
1259     } else {
1260
1261         found_unknown_content($c);
1262
1263     }
1264
1265     return $c;
1266 }
1267
1268 sub handle_loglevel_gif_deanimate ($) {
1269
1270     my $content = shift;
1271     our $t;
1272     our %req;
1273     our %h;
1274
1275     if ($content =~ m/Success! GIF shrunk from (\d+) bytes to (\d+)\./) {
1276
1277         my $bytes_from = $1;
1278         my $bytes_to = $2;
1279         # Gif-Deanimate: Success! GIF shrunk from 205 bytes to 133.
1280         $content =~ s@$bytes_from@$h{'Number'}$bytes_from$h{'Standard'}@;
1281         # XXX: Do we need g in case of ($1 == $2)?
1282         $content =~ s@$bytes_to@$h{'Number'}$bytes_to$h{'Standard'}@;
1283
1284     } elsif ($content =~ m/GIF (not) changed/) {
1285
1286         # Gif-Deanimate: GIF not changed.
1287         return '' if SUPPRESS_GIF_NOT_CHANGED;
1288         $content =~ s@($1)@$h{'not'}$1$h{'Standard'}@;
1289
1290     } elsif ($content =~ m/^failed! \(gif parsing\)/) {
1291
1292         # failed! (gif parsing)
1293         # XXX: Replace this error message with something less stupid 
1294         $content =~ s@(failed!)@$h{'error'}$1$h{'Standard'}@;
1295
1296     } elsif ($content =~ m/^Need to de-chunk first/) {
1297
1298         # Need to de-chunk first
1299         return '' if SUPPRESS_NEED_TO_DE_CHUNK_FIRST;
1300
1301     } elsif ($content =~ m/^(?:No GIF header found|failed while parsing)/) {
1302
1303         # No GIF header found (XXX: Did I ever commit this?)
1304         # failed while parsing 195 134747048 (XXX: never commited)
1305
1306         # Ignore these for now
1307
1308     } else {
1309
1310         found_unknown_content($content);
1311
1312     }
1313
1314     return $content;
1315 }
1316
1317 sub handle_loglevel_request ($) {
1318
1319     my $content = shift;
1320     our $t;
1321     our %req;
1322     our %h;
1323     our %reason_colours;
1324
1325     if ($content =~ m/crunch! /) {
1326
1327         # config.privoxy.org/send-stylesheet crunch! (CGI Call)
1328
1329         # Highlight crunch reasons
1330         foreach my $reason (keys %reason_colours) {
1331             $content =~ s@\(($reason)\)@$reason_colours{$reason}($1)$h{'Standard'}@g;
1332         }
1333         # Highlight request URL domain and ditch 'crunch!'
1334         $content = highlight_matched_pattern($content, 'request_', '[^ ]*(?= crunch!)');
1335         $content =~ s@ crunch!@@;
1336
1337     } elsif ($content =~ m/\[too long, truncated\]$/) {
1338
1339         # config.privoxy.org/edit-actions-submit?f=3&v=1176116716&s=7&Submit=Submit[...]&filter... [too long, truncated]
1340         $content = highlight_matched_pattern($content, 'request_', '^.*(?=\.\.\. \[too long, truncated\]$)');
1341
1342     } elsif ($content =~ m/(.*)/) { # XXX: Pretty stupid
1343
1344         # trac.vidalia-project.net/wiki/Volunteer?format=txt
1345         $content = h('request_') . $content . h('Standard');
1346
1347     } else {  # XXX: Nop
1348
1349         found_unknown_content($content);
1350
1351     }
1352             
1353     return $content;
1354 }
1355
1356 sub handle_loglevel_crunch ($) {
1357
1358     my $content = shift;
1359     our %h;
1360     our %reason_colours;
1361
1362     # Highlight crunch reason
1363     foreach my $reason (keys %reason_colours) {
1364         $content =~ s@($reason)@$reason_colours{$reason}$1$h{'Standard'}@g;
1365     }
1366
1367     if ($content =~ m/\[too long, truncated\]$/) {
1368
1369         # Blocked: config.privoxy.org/edit-actions-submit?f=3&v=1176116716&s=7&Submit=Submit\
1370         #  [...]&filter... [too long, truncated]
1371         $content = highlight_matched_pattern($content, 'request_', '^.*(?=\.\.\. \[too long, truncated\]$)');
1372
1373     } else {
1374
1375         # Blocked: http://ads.example.org/
1376         $content = highlight_matched_pattern($content, 'request_', '(?<=: ).*');
1377     }
1378
1379     return $content;
1380 }
1381
1382 sub handle_loglevel_connect ($) {
1383
1384     my $c = shift;
1385     our $t;
1386     our %req;
1387     our %h;
1388
1389     if ($c =~ m/^via [^\s]+ to: [^\s]+/) {
1390
1391         # Connect: via 10.0.0.1:8123 to: www.example.org.noconnect
1392
1393         $c = highlight_matched_host($c, '(?<=via )[^\s]+');
1394         $c = highlight_matched_host($c, '(?<=to: )[^\s]+');
1395
1396     } elsif ($c =~ m/^connect to: .* failed: .*/) {
1397
1398         # connect to: www.example.org.noconnect failed: Operation not permitted
1399
1400         $c = highlight_matched_host($c, '(?<=connect to: )[^\s]+');
1401
1402         $c =~ s@(?<=failed: )(.*)@$h{'error'}$1$h{'Standard'}@;
1403
1404     } elsif ($c =~ m/^to ([^\s]*)( successful)?$/) {
1405
1406         # Connect: to www.nzherald.co.nz successful
1407         # Connect: to archiv.radiotux.de
1408
1409         return '' if SUPPRESS_SUCCESSFUL_CONNECTIONS;
1410         $c = highlight_matched_host($c, '(?<=to )[^\s]+');
1411
1412     } elsif ($c =~ m/^to ([^\s]*)$/) {
1413
1414         # Connect: to lists.sourceforge.net:443
1415
1416         $c = highlight_matched_host($c, '(?<=to )[^\s]+');
1417
1418     } elsif ($c =~ m/^accepted connection from .*/ or
1419              $c =~ m/^OK/) {
1420
1421         # accepted connection from 10.0.0.1
1422         # Privoxy 3.0.6 and earlier just say:
1423         # OK
1424         return '' if SUPPRESS_ACCEPTED_CONNECTIONS;
1425         $c = highlight_matched_host($c, '(?<=connection from ).*');
1426
1427     } elsif ($c =~ m/^write header to: .* failed:/) {
1428
1429         # write header to: 10.0.0.1 failed: Broken pipe
1430
1431         $c = highlight_matched_host($c, '(?<=write header to: )[^\s]*');
1432         $c =~ s@(?<=failed: )(.*)@$h{'Error'}$1$h{'Standard'}@;
1433
1434     } elsif ($c =~ m/^write header to client failed:/) {
1435
1436         # write header to client failed: Broken pipe
1437         # XXX: Stil in use?
1438         $c =~ s@(?<=failed: )(.*)@$h{'Error'}$1$h{'Standard'}@;
1439
1440     } elsif ($c =~ m/^socks4_connect:/) {
1441
1442         # socks4_connect: SOCKS request rejected or failed.
1443         $c =~ s@(?<=socks4_connect: )(.*)@$h{'Error'}$1$h{'Standard'}@;
1444
1445     } elsif ($c =~ m/^Listening for new connections/ or
1446              $c =~ m/^accept connection/) {
1447         # XXX: Highlight?
1448         # Privoxy versions above 3.0.6 say:
1449         # Listening for new connections ...
1450         # earlier versions say:
1451         # accept connection ...
1452         return '';
1453
1454     } elsif ($c =~ m/^accept failed:/) {
1455
1456         $c =~ s@(?<=accept failed: )(.*)@$h{'Error'}$1$h{'Standard'}@;
1457
1458     } elsif ($c =~ m/^Overriding forwarding settings/) {
1459
1460         # Overriding forwarding settings based on 'forward 10.0.0.1:8123'
1461         $c =~ s@(?<=based on \')(.*)(?=\')@$h{'configuration-line'}$1$h{'Standard'}@;
1462
1463     } elsif ($c =~ m/^Denying suspicious CONNECT request from/) {
1464
1465         # Denying suspicious CONNECT request from 10.0.0.1
1466         $c = highlight_matched_host($c, '(?<=from )[^\s]+'); # XXX: not an URL
1467
1468     } elsif ($c =~ m/^socks5_connect:/) {
1469     
1470         $c =~ s@(?<=socks5_connect: )(.*)@$h{'error'}$1$h{'Standard'}@;
1471
1472     } elsif ($c =~ m/^Created new connection to/) {
1473
1474         # Created new connection to www.privoxy.org:80 on socket 11.
1475         $c = highlight_matched_host($c, '(?<=connection to )[^\s]+');
1476         $c =~ s@(?<=on socket )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1477
1478     } elsif ($c =~ m/^Found reusable socket/) {
1479
1480         # Found reusable socket 9 for www.privoxy.org:80 in slot 0.
1481         $c =~ s@(?<=Found reusable socket )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1482         $c = highlight_matched_host($c, '(?<=for )[^\s]+');
1483         $c =~ s@(?<=in slot )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1484
1485     } elsif ($c =~ m/^Marking open socket/) {
1486
1487         # Marking open socket 9 for www.privoxy.org:80 in slot 0 as unused.
1488         $c =~ s@(?<=Marking open socket )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1489         $c = highlight_matched_host($c, '(?<=for )[^\s]+');
1490         $c =~ s@(?<=in slot )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1491
1492     } elsif ($c =~ m/^No reusable/) {
1493
1494         # No reusable socket for addons.mozilla.org:443 found. Opening a new one.
1495         $c = highlight_matched_host($c, '(?<=for )[^\s]+');
1496
1497     } elsif ($c =~ m/^(Remembering|Forgetting) socket/) {
1498
1499         # Remembering socket 13 for www.privoxy.org:80 in slot 0.
1500         # Forgetting socket 38 for www.privoxy.org:80 in slot 5.
1501
1502         $c =~ s@(?<=socket )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1503         $c = highlight_matched_host($c, '(?<=for )[^\s]+');
1504         $c =~ s@(?<=in slot )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1505
1506     } elsif ($c =~ m/^Socket/) {
1507
1508         # Socket 16 already forgotten or never remembered.
1509         $c =~ s@(?<=Socket )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1510
1511     } elsif ($c =~ m/^The connection to/) {
1512
1513         # The connection to www.privoxy.org:80 in slot 6 timed out. Closing socket 19. Timeout is: 61.
1514         # 3.0.15 and later:
1515         # The connection to 1.bp.blogspot.com:80 in slot 0 timed out. Closing socket 5.\
1516         #  Timeout is: 1. Assumed latency: 4.
1517         # The connection to 10.0.0.1:80 in slot 0 is no longer usable. Closing socket 4.
1518         $c = highlight_matched_host($c, '(?<=connection to )[^\s]+');
1519         $c =~ s@(?<=in slot )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1520         $c =~ s@(?<=Closing socket )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1521         $c =~ s@(?<=Timeout is: )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1522         $c =~ s@(?<=Assumed latency: )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1523
1524     } elsif ($c =~ m/^Waiting for \d/) {
1525
1526         # Waiting for 1 connections to timeout.
1527         $c =~ s@(?<=^Waiting for )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1528
1529     } elsif ($c =~ m/^Initialized/) {
1530
1531         # Initialized 20 socket slots.
1532         $c =~ s@(?<=Initialized )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1533
1534     } elsif ($c =~ m/^Done reading from server/) {
1535
1536         # Done reading from server. Expected content length: 24892. \
1537         #  Actual content length: 24892. Most recently received: 4412.
1538         # 3.0.15 and later:
1539         # Done reading from server. Expected content length: 24892. \
1540         #  Actual content length: 24892. Bytes most recently read: 4412.
1541         $c =~ s@(?<=Expected content length: )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1542         $c =~ s@(?<=Actual content length: )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1543         $c =~ s@(?<=received: )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1544         $c =~ s@(?<=read: )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1545
1546     } elsif ($c =~ m/^Continuing buffering headers/) {
1547
1548         # Continuing buffering headers. byte_count: 19. header_offset: 517. len: 536.
1549         $c =~ s@(?<=byte_count: )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1550         $c =~ s@(?<=header_offset: )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1551         $c =~ s@(?<=len: )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1552         # 3.0.15 and later:
1553         # Continuing buffering headers. Bytes most recently read: %d.
1554         $c =~ s@(?<=read: )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1555
1556     } elsif ($c =~ m/^Received \d+ bytes while/) {
1557
1558         # Received 206 bytes while expecting 12103.
1559         $c =~ s@(?<=Received )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1560         $c =~ s@(?<=expecting )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1561
1562     } elsif ($c =~ m/^Connection from/) {
1563
1564         # Connection from 81.163.28.218 dropped due to ACL
1565         $c =~ s@(?<=^Connection from )((?:\d+\.?){4})@$h{'Number'}$1$h{'Standard'}@;
1566
1567     } elsif ($c =~ m/^(?:Reusing|Closing) server socket \d./ or
1568              $c =~ m/^No additional client request/) {
1569
1570         # Reusing server socket 4. Opened for 10.0.0.1.
1571         # Closing server socket 2. Opened for 10.0.0.1.
1572         # No additional client request received in time. \
1573         #  Closing server socket 4, initially opened for 10.0.0.1.
1574
1575         $c =~ s@(?<=server socket )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1576         $c = highlight_matched_host($c, '(?<=for )[^\s]+(?=\.$)');
1577
1578     } elsif ($c =~ m/^Connected to /) {
1579
1580         # Connected to tor-jail[10.0.0.2]:9050.
1581
1582         $c = highlight_matched_host($c, '(?<=\[)[^\]]+');
1583         $c = highlight_matched_host($c, '(?<=Connected to )[^\[\s]+');
1584         $c =~ s@(?<=\]:)(\d+)@$h{'Number'}$1$h{'Standard'}@;
1585
1586     } elsif ($c =~ m/^Could not connect to /) {
1587
1588         # Could not connect to [10.0.0.1]:80.
1589
1590         $c = highlight_matched_host($c, '(?<=\[)[^\]]+');
1591         $c =~ s@(?<=\]:)(\d+)@$h{'Number'}$1$h{'Standard'}@;
1592
1593     } elsif ($c =~ m/^Waiting for the next client request/ or
1594              $c =~ m/^The connection on server socket/ ) {
1595
1596         # Waiting for the next client request. Keeping the server socket 5 to 10.0.0.1 open.
1597         # The connection on server socket 6 to upload.wikimedia.org isn't reusable. Closing.
1598
1599         $c =~ s@(?<=server socket )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1600         $c = highlight_matched_host($c, '(?<=to )[^\s]+');
1601
1602     } elsif ($c =~ m/^Marking the server socket/) {
1603
1604         # Marking the server socket 7 tainted.
1605
1606         $c =~ s@(?<=server socket )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1607
1608     } elsif ($c =~ m/^Reduced expected bytes to /) {
1609
1610         # Reduced expected bytes to 0 to account for the 1542 ones we already got.
1611         $c =~ s@(?<=bytes to )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1612         $c =~ s@(?<=for the )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1613
1614     } elsif ($c =~ m/^Looks like we rea/ or
1615              $c =~ m/^Unsetting keep-alive flag/ or
1616              $c =~ m/^No connections to wait/ or
1617              $c =~ m/^Client request arrived in time or the client closed the connection/ or
1618              $c =~ m/^Complete client request received/ or
1619              $c =~ m/^Possible pipeline attempt detected./ or
1620              $c =~ m/^POST request detected. The connection will not be kept alive./ or
1621              $c =~ m/^The server still wants to talk, but the client hung up on us./ or
1622              $c =~ m/^The server didn't specify how long the connection will stay open/ or
1623              $c =~ m/^There might be a request body. The connection will not be kept alive/) {
1624
1625         # Looks like we reached the end of the last chunk. We better stop reading.
1626         # Looks like we read the end of the last chunk together with the server \
1627         #  headers. We better stop reading.
1628         # Unsetting keep-alive flag.
1629         # No connections to wait for left.
1630         # Client request arrived in time or the client closed the connection.
1631         # Complete client request received
1632         # Possible pipeline attempt detected. The connection will not be \
1633         #  kept alive and we will only serve the first request.
1634         # POST request detected. The connection will not be kept alive.
1635         # The server still wants to talk, but the client hung up on us.
1636         # The server didn't specify how long the connection will stay open. Assume it's only a second.
1637         # There might be a request body. The connection will not be kept alive.
1638
1639     } else {
1640
1641         found_unknown_content($c);
1642
1643     }
1644             
1645     return $c;
1646 }
1647
1648
1649 sub handle_loglevel_info ($) {
1650
1651     my $c = shift;
1652     our $t;
1653     our %req;
1654     our %h;
1655  
1656     if ($c =~ m/^Rewrite detected:/) {
1657
1658         # Rewrite detected: GET http://10.0.0.2:88/blah.txt HTTP/1.1
1659         $c = highlight_matched_request_line($c, '(?<=^Rewrite detected: ).*');
1660
1661     } elsif ($c =~ m/^Decompress(ing deflated|ion didn)/ or
1662              $c =~ m/^Compressed content detected/ or
1663              $c =~ m/^Tagger/
1664             ) {
1665         # Decompressing deflated iob: 117
1666         # Decompression didn't result in any content.
1667         # Compressed content detected, content filtering disabled. Consider recompiling Privoxy\
1668         #  with zlib support or enable the prevent-compression action.
1669         # Tagger 'complete-url' created empty tag. Ignored.
1670
1671         # Ignored for now
1672
1673     } elsif ($c =~ m/^(Re)?loading configuration file /) {
1674
1675         # loading configuration file '/usr/local/etc/privoxy/config':
1676         # Reloading configuration file '/usr/local/etc/privoxy/config'
1677         $c =~ s@(?<=loading configuration file \')([^\']*)@$h{'file'}$1$h{'Standard'}@;
1678
1679     } elsif ($c =~ m/^exiting by signal/) {
1680         
1681         # exiting by signal 15 .. bye
1682         $c =~ s@(?<=exiting by signal )(\d+)@$h{'signal'}$1$h{'Standard'}@;
1683
1684     } elsif ($c =~ m/^Privoxy version/) {
1685         
1686         # Privoxy version 3.0.7
1687         $c =~ s@(?<=^Privoxy version )(\d+\.\d+\.\d+)$@$h{'version'}$1$h{'Standard'}@;
1688
1689     } elsif ($c =~ m/^Program name: /) {
1690
1691         # Program name: /usr/local/sbin/privoxy
1692         $c =~ s@(?<=Program name: )(.*)@$h{'program-name'}$1$h{'Standard'}@;
1693
1694     } elsif ($c =~ m/^Listening on port /) {
1695
1696         # Listening on port 8118 on IP address 10.0.0.1
1697         $c =~ s@(?<=Listening on port )(\d+)@$h{'port'}$1$h{'Standard'}@;
1698         $c =~ s@(?<=on IP address )(.*)@$h{'ip-address'}$1$h{'Standard'}@;
1699
1700     } elsif ($c =~ m/^\(Re-\)Open(?:ing)? logfile/) {
1701
1702         # (Re-)Open logfile /var/log/privoxy/privoxy.log
1703         $c =~ s@(?<=Open logfile )(.*)@$h{'file'}$1$h{'Standard'}@;
1704
1705     } elsif ($c =~ m/^(Request from|Malformed server response detected)/) {
1706
1707         # Request from 10.0.0.1 denied. limit-connect{,} doesn't allow CONNECT requests to port 443.
1708         # Request from 10.0.0.1 marked for blocking. limit-connect{,} doesn't allow CONNECT requests to port 443.
1709         # Malformed server response detected. Downgrading to HTTP/1.0 impossible.
1710
1711         $c =~ s@(?<=Request from )([^\s]*)@$h{'ip-address'}$1$h{'Standard'}@;
1712         $c =~ s@(denied|blocking)@$h{'warning'}$1$h{'Standard'}@;
1713         $c =~ s@(CONNECT)@$h{'method'}$1$h{'Standard'}@;
1714         $c =~ s@(?<=to port )(\d+)@$h{'port'}$1$h{'Standard'}@;
1715
1716     } elsif ($c =~ m/^Status code/) {
1717
1718         # Status code 304 implies no body.
1719         $c =~ s@(?<=Status code )(\d+)@$h{'status-code'}$1$h{'Standard'}@;
1720
1721     } elsif ($c =~ m/^Method/) {
1722
1723         # Method HEAD implies no body.
1724         $c =~ s@(?<=Method )([^\s]+)@$h{'method'}$1$h{'Standard'}@;
1725
1726     } elsif ($c =~ m/^Buffer limit reached while extending /) {
1727
1728         # Buffer limit reached while extending the buffer (iob). Needed: 4197470. Limit: 4194304
1729         $c =~ s@(?<=Needed: )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1730         $c =~ s@(?<=Limit: )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1731
1732     } elsif ($c =~ m/^No logfile configured/ or
1733              $c =~ m/^Malformerd HTTP headers detected and MS IIS5 hack enabled/ or
1734              $c =~ m/^Invalid \"chunked\" transfer/ or
1735              $c =~ m/^Support for/ or
1736              $c =~ m/^Flushing header and buffers/
1737              ) {
1738
1739         # No logfile configured. Please enable it before reporting any problems.
1740         # Malformerd HTTP headers detected and MS IIS5 hack enabled. Expect an invalid \
1741         #  response or even no response at all.
1742         # No logfile configured. Logging disabled.
1743         # Invalid "chunked" transfer encoding detected and ignored.
1744         # Support for 'Connection: keep-alive' is experimental, incomplete and\
1745         #  known not to work properly in some situations.
1746         # Flushing header and buffers. Stepping back from filtering.
1747
1748     } else {
1749
1750         found_unknown_content($c);
1751
1752     }
1753
1754     return $c;
1755 }
1756
1757 sub handle_loglevel_cgi ($) {
1758
1759     my $c = shift;
1760     our $t;
1761     our %req;
1762     our %h;
1763
1764     if ($c =~ m/^Granting access to/) {
1765       
1766         #Granting access to http://config.privoxy.org/send-stylesheet, referrer http://p.p/ is trustworthy.
1767
1768     } elsif ($c =~ m/^Substituting: s(.)/) {
1769       
1770         # Substituting: s/@else-not-FEATURE_ZLIB@.*@endif-FEATURE_ZLIB@//sigTU
1771         # XXX: prone to span several lines
1772
1773         my $delimiter = $1;
1774         #$c =~ s@(?<=failed: )(.*)@$h{'error'}$1$h{'Standard'}@;
1775         $c =~ s@(?!<=\\)($delimiter)@$h{'pcrs-delimiter'}$1$h{'Standard'}@g; # XXX: Too aggressive
1776         #$c =~ s@(?!<=\\)($1)@$h{'pcrs-delimiter'}$1$h{'Standard'}@g;
1777     }
1778
1779     return $c;
1780 }
1781
1782 sub handle_loglevel_force ($) {
1783
1784     my $c = shift;
1785     our $t;
1786     our %req;
1787     our %h;
1788
1789     if ($c =~ m/^Ignored force prefix in request:/) {
1790       
1791         # Ignored force prefix in request: "GET http://10.0.0.1/PRIVOXY-FORCE/block HTTP/1.1"
1792         $c =~ s@^(Ignored)@$h{'ignored'}$1$h{'Standard'}@;
1793         $c = highlight_matched_request_line($c, '(?<=request: ")[^"]*');
1794
1795     } elsif ($c =~ m/^Enforcing request:/) {
1796       
1797         # Enforcing request: "GET http://10.0.0.1/block HTTP/1.1".
1798         $c = highlight_matched_request_line($c, '(?<=request: ")[^"]*');
1799
1800     } else {
1801
1802         found_unknown_content($c);
1803
1804     }
1805
1806     return $c;
1807 }
1808
1809 sub handle_loglevel_error ($) {
1810
1811     my $c = shift;
1812     our %h;
1813
1814     if ($c =~ m/^Empty server or forwarder response received on socket \d+./) {
1815
1816         # Empty server or forwarder response received on socket 4.
1817         $c =~ s@(?<=on socket )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1818     }
1819     # XXX: There are probably more messages that deserve highlighting.
1820
1821     return $c;
1822 }
1823
1824
1825 sub handle_loglevel_ignore ($) {
1826     return shift;
1827 }
1828
1829 ################################################################################
1830 # Functions that actually print stuff
1831 ################################################################################
1832
1833 sub print_clf_message () {
1834
1835     our ($ip, $timestamp, $request_line, $status_code, $size);
1836     our %h;
1837     my $output = '';
1838
1839     return if DEBUG_SUPPRESS_LOG_MESSAGES;
1840
1841     # Rebuild highlighted
1842     $output .= $h{'Number'} . $ip . $h{'Standard'};
1843     $output .= " - - ";
1844     $output .= "[" . $h{'Timestamp'} . $timestamp . $h{'Standard'} . "]";
1845     $output .= " ";
1846     $output .= "\"" . highlight_request_line("$request_line") . "\"";
1847     $output .= " ";
1848     $output .= $h{'Status'} . $status_code . $h{'Standard'};
1849     $output .= " ";
1850     $output .= $h{'Number'} . $size . $h{'Standard'};
1851     $output .= get_line_end();
1852
1853     print $output;
1854 }
1855
1856 sub print_non_clf_message ($) {
1857
1858     our %req;
1859     our %thread_colours;
1860     our %h;
1861     our $t;
1862     our $time_colour_index;
1863     our @time_colours;
1864     my $output;
1865     my $content = shift;
1866     my ($day, $time_stamp, $msecs, $thread, $log_level)
1867      = ($req{$t}{'day'}, $req{$t}{'time-stamp'}, $req{$t}{'msecs'}, $t, $req{$t}{'log-level'} );
1868
1869     return if DEBUG_SUPPRESS_LOG_MESSAGES;
1870
1871     $output .= $h{"Standard"} unless cli_option_is_set('html-output');
1872     #    $output .= "$day ";
1873     $output .= $time_colours[$time_colour_index % 2]; 
1874
1875     $output .= $time_stamp;
1876     $output .= ".$msecs" unless cli_option_is_set('no-msecs');
1877     $output .= $h{"Standard"};
1878     $output .= " ";
1879     $output .= $thread_colours{$thread} if (defined($thread_colours{$thread}));
1880     $output .= $thread;
1881     $output .= $h{"Standard"} . " ";
1882     $output .= $h{$log_level} if (defined($h{$log_level}));
1883     $output .= $log_level;
1884     $output .= $h{"Standard"} . ": ";
1885     $output .= "$content";
1886     $output .= get_line_end();
1887
1888     print $output;
1889 }
1890
1891 sub parse_loop () {
1892
1893     our $t;
1894     our %req; # request data from previous lines
1895     our %h;
1896     our %thread_colours;
1897     our @all_colours;
1898     our @time_colours;
1899     our $thread_colour_index = 0;
1900     our $header_colour_index = 0;
1901     our $time_colour_index = 0;
1902
1903     my ($day, $time_stamp, $thread, $log_level, $content, $c, $msecs);
1904     my $last_msecs  = 0;
1905     my $last_thread = 0;
1906     my $last_timestamp = 0;
1907     my $output;
1908     my $filters_that_did_nothing;
1909     my $key;
1910     my $time_colour;
1911     our $no_special_header_highlighting;
1912     $time_colour = paint_it('white');
1913     my %log_level_count;
1914
1915     my %log_level_handlers = (
1916         'Re-Filter'         => \&handle_loglevel_re_filter,
1917         'Header'            => \&handle_loglevel_header,
1918         'Connect'           => \&handle_loglevel_connect,
1919         'Redirect'          => \&handle_loglevel_redirect,
1920         'Request'           => \&handle_loglevel_request,
1921         'Crunch'            => \&handle_loglevel_crunch,
1922         'Gif-Deanimate'     => \&handle_loglevel_gif_deanimate,
1923         'Info'              => \&handle_loglevel_info,
1924         'CGI'               => \&handle_loglevel_cgi,
1925         'Force'             => \&handle_loglevel_force,
1926         'Error'             => \&handle_loglevel_error,
1927         'Fatal error'       => \&handle_loglevel_ignore,
1928         'Writing'           => \&handle_loglevel_ignore,
1929         'Unknown log level' => \&handle_loglevel_ignore,
1930     );
1931
1932     while (<>) {
1933  
1934         $output = '';
1935
1936         if (m/^(\w{3} \d{2}) (\d\d:\d\d:\d\d)\.?(\d+)? (?:Privoxy\()?([^\)\s]*)[\)]? ([\w -]*): (.*?)\r?$/) {
1937             # XXX: Put in req hash?
1938             $day = $1;
1939             $time_stamp = $2;
1940             $msecs = $3 ? $3 : 0; # Only the cool kids have micro second resolution
1941             $log_level = $5;
1942             $content = $c = $6;
1943             $thread = $t = $4;
1944
1945             $req{$t}{'day'} = $day;
1946             $req{$t}{'time-stamp'} = $time_stamp;
1947             $req{$t}{'msecs'} = $msecs; # Only the cool kids have micro second resolution;
1948             $req{$t}{'log-level'} = $log_level;
1949             $req{$t}{'content'} = $content;
1950             $req{$t}{'log-message'} = $_;
1951             $no_special_header_highlighting = 0;
1952
1953             $log_level_count{$log_level}++;
1954
1955             if (defined($log_level_handlers{$log_level})) {
1956
1957                 $content = $log_level_handlers{$log_level}($content);
1958
1959             } else {
1960
1961                 die "No handler found for log level \"$log_level\"\n";
1962
1963             }
1964
1965             # Highlight Truncations    
1966             if (m/\.\.\. \[(too long, truncated)/) {
1967                 $content =~ s@($1)@$h{'Truncation'}$1$h{'Standard'}@g;
1968             }
1969
1970             next unless $content;
1971
1972             # Register threads to keep the colour constant
1973             if (!defined($thread_colours{$thread})) {
1974                 $thread_colours{$thread} = $all_colours[$thread_colour_index % @all_colours];
1975                 $thread_colour_index++;
1976             }
1977
1978             # Switch timestamp colour if timestamps differ
1979             if ($msecs != $last_msecs || !($time_stamp =~ m/$last_timestamp/)) {
1980                debug_message("Tick tack!") if DEBUG_TICKS;
1981                $time_colour = $time_colours[$time_colour_index % 2]; 
1982                $time_colour_index++
1983             }
1984
1985             $last_msecs = $msecs;
1986             $last_thread = $thread;
1987             $last_timestamp = $time_stamp;
1988
1989             print_non_clf_message($content);
1990
1991         } elsif (m/^((?:\d+\.\d+\.\d+\.\d+|[:\d]+)) - - \[(.*)\] "(.*)" (\d+) (\d+)/) {
1992
1993             # LOG_LEVEL_CLF lines look like this
1994             # 61.152.239.32 - - [04/Mar/2007:18:28:23 +0100] "GET \
1995             #  http://ad.yieldmanager.com/imp?z=1&Z=120x600&s=109339&u=http%3A%2F%2Fwww.365loan.co.uk%2F&r=1\
1996             #  HTTP/1.1" 403 1730
1997             our ($ip, $timestamp, $request_line, $status_code, $size) = ($1, $2, $3, $4, $5);
1998
1999             print_clf_message();
2000     
2001         } else {
2002
2003             # Some Privoxy log messages span more than one line,
2004             # usually to dump lots of content that doesn't need any syntax highlighting.
2005             # XXX: add mechanism to forward these lines to the right handler anyway.
2006             chomp();
2007             unless (DEBUG_SUPPRESS_LOG_MESSAGES or (SUPPRESS_EMPTY_LINES and m/^\s+$/)) {
2008                 print and print get_line_end(); # unless (SUPPRESS_EMPTY_LINES and m/^\s+$/);
2009             }
2010         }
2011     }
2012
2013     if (cli_option_is_set('statistic')) {
2014         foreach (keys %log_level_count) {
2015             print $_ . ": " . $log_level_count{$_} . " ";
2016         }
2017     }
2018 }
2019
2020 sub VersionMessage {
2021     my $version_message;
2022
2023     $version_message .= 'Privoxy-Log-Parser ' . PRIVOXY_LOG_PARSER_VERSION  . "\n";
2024     $version_message .= 'Copyright (C) 2007-2009 Fabian Keil <fk@fabiankeil.de>' . "\n";
2025     $version_message .= 'http://www.fabiankeil.de/sourcecode/privoxy-log-parser/' . "\n";
2026
2027     print $version_message;
2028 }
2029
2030 sub get_cli_options () {
2031
2032     our %cli_options = (
2033         'html-output'              => CLI_OPTION_DEFAULT_TO_HTML_OUTPUT,
2034         'title'                    => CLI_OPTION_TITLE,
2035         'no-syntax-highlighting'   => CLI_OPTION_NO_SYNTAX_HIGHLIGHTING,
2036         'no-embedded-css'          => CLI_OPTION_NO_EMBEDDED_CSS,
2037         'no-msecs'                 => CLI_OPTION_NO_MSECS,
2038         'show-ineffective-filters' => CLI_OPTION_SHOW_INEFFECTIVE_FILTERS,
2039         'accept-unknown-messages'  => CLI_OPTION_ACCEPT_UNKNOWN_MESSAGES,
2040         'statistic'                => CLI_OPTION_STATISTIC,
2041     ); 
2042
2043     GetOptions (
2044         'html-output'              => \$cli_options{'html-output'},
2045         'title'                    => \$cli_options{'title'},
2046         'no-syntax-highlighting'   => \$cli_options{'no-syntax-highlighting'},
2047         'no-embedded-css'          => \$cli_options{'no-embedded-css'},
2048         'no-msecs'                 => \$cli_options{'no-msecs'},
2049         'show-ineffective-filters' => \$cli_options{'show-ineffective-filters'},
2050         'accept-unknown-messages'  => \$cli_options{'accept-unknown-messages'},
2051         'statistic'                => \$cli_options{'statistic'},
2052         'version'                  => sub { VersionMessage && exit(0) }
2053    );
2054 }
2055
2056
2057
2058 ################################################################################
2059 # main
2060 ################################################################################
2061 sub main () {
2062
2063     get_cli_options();
2064     set_background(DEFAULT_BACKGROUND);
2065     prepare_our_stuff();
2066
2067     print_intro();
2068
2069     parse_loop();
2070
2071     print_outro();
2072 }
2073
2074 main();
2075
2076 =head1 NAME
2077
2078 B<privoxy-log-parser> - A parser and syntax-highlighter for Privoxy log messages
2079
2080 =head1 SYNOPSIS
2081
2082 B<privoxy-log-parser> [B<--accept-unknown-messages>] [B<--html-output>]
2083 [B<--no-msecs>] [B<--no-syntax-higlighting>] [B<--show-ineffective-filters>]
2084 [B<--version>]
2085
2086 =head1 DESCRIPTION
2087
2088 B<privoxy-log-parser> reads Privoxy log messages and
2089
2090 - syntax-highlights recognized lines,
2091
2092 - reformats some of them for easier comprehension,
2093
2094 - filters out less useful messages, and
2095
2096 - (in some cases) calculates additional information,
2097   like the compression ratio or how a filter affected
2098   the content size.
2099  
2100 With B<privoxy-log-parser> you should be able to increase Privoxy's log level
2101 without getting confused by the resulting amount of output. For example for
2102 "debug 64" B<privoxy-log-parser> will (by default) only show messages that
2103 affect the content. If a filter doesn't cause any hits, B<privoxy-log-parser>
2104 will hide the "filter foo caused 0 hits" message.
2105
2106 =head1 OPTIONS
2107
2108 [B<--accept-unknown-messages>] Don't print warnings in case of unknown messages,
2109 just don't highlight them.
2110
2111 [B<--html-output>] Use HTML and CSS for the syntax highlighting. If this option is
2112 omitted, ANSI escape sequences are used unless B<--no-syntax-highlighting> is active.
2113 This option is only intended to make embedding log excerpts in web pages easier.
2114 It does not escape any input!
2115
2116 [B<--no-msecs>] Don't expect milisecond resolution
2117
2118 [B<--no-syntax-highlighting>] Disable syntax-highlighting. Useful when
2119 the filtered output is piped into less in which case the ANSI control
2120 codes don't work, or if the terminal itself doesn't support the control
2121 codes.
2122
2123 [B<--show-ineffective-filters>] Don't suppress log lines for filters
2124 that didn't modify the content.
2125
2126 [B<--version>] Print version and exit.
2127
2128 =head1 EXAMPLES
2129
2130 To monitor a log file:
2131
2132 tail -F /usr/jails/privoxy-jail/var/log/privoxy/privoxy.log | B<privoxy-log-parser>
2133
2134 Replace '-F' with '-f' if your tail implementation lacks '-F' support
2135 or if the log won't get rotated anyway. The log file location depends
2136 on your system (Doh!).
2137
2138 To monitor Privoxy without having it write to a log file:
2139
2140 privoxy --no-daemon /usr/jails/privoxy-jail/usr/local/etc/privoxy/config 2>&1 | B<privoxy-log-parser>
2141
2142 Again, the config file location depends on your system. Output redirection
2143 depends on your shell, the above works with bourne shells.
2144
2145 To read a processed Privoxy log file from top to bottom, letting the content
2146 scroll by slightly faster than you can read:
2147
2148 B<privoxy-log-parser> < /usr/jails/privoxy-jail/var/log/privoxy/privoxy.log
2149
2150 This is probably only useful to fill screens in the background of haxor movies.
2151
2152 =head1 CAVEATS
2153
2154 Syntax highlighting with ANSI escape sequences will look strange
2155 if your background color isn't black.
2156
2157 Some messages aren't recognized yet and will not be fully highlighted.
2158
2159 B<privoxy-log-parser> is developed with Privoxy 3.0.7 or later in mind,
2160 using earlier Privoxy versions will probably result in an increased amount
2161 of unrecognized log lines.
2162
2163 Privoxy's log files tend to be rather large. If you use HTML
2164 highlighting some browsers can't handle them, get confused and
2165 will eventually crash because of segmentation faults or unexpected
2166 exceptions. This is a problem in the browser and not B<privoxy-log-parser>'s
2167 fault.
2168
2169 =head1 BUGS
2170
2171 Many settings can't be controlled through command line options yet.
2172
2173 =head1 SEE ALSO
2174
2175 privoxy(1)
2176
2177 =head1 AUTHOR
2178
2179 Fabian Keil <fk@fabiankeil.de>
2180
2181 =cut