Update version-related entities.
[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.113 2008/08/13 16:27:07 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 higlight_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 higlight_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 = higlight_matched_url($rl, '[^\s]*(?=\sHTTP)');
661         } else {
662             $rl = higlight_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 = higlight_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
692     # TODO: Mark different status codes differently
693
694     if ($rl =~ m/(HTTP\/\d\.\d) (\d+) (.*)/) {
695         ($http_version, $status_code, $status_message) = ($1, $2, $3);
696     } else {
697         debug_message ("Can't parse response line: $rl") and die 'Fix this';
698     }
699
700     # Rebuild highlighted
701     $rl= "";
702     $rl .= h('http-version') . $http_version . h('Standard');
703     $rl .= " ";
704     $rl .= h('status-code') . $status_code . h('Standard');
705     $rl .= " ";
706     $rl .= h('status-message') . $status_message . h('Standard');
707
708     return $rl;
709 }
710
711 sub higlight_matched_url ($$) {
712
713     my $result = shift; # XXX: Stupid name;
714     my $regex = shift;
715
716     #print "Got $result, regex ($regex)\n";
717
718     if ($result =~ m@(.*?)($regex)(.*)@) {
719         $result = $1 . highlight_url($2) . $3;
720         #print "Now the result is $result\n";
721     }
722
723     return $result;
724 }
725
726 sub higlight_matched_host ($$) {
727
728     my $result = shift; # XXX: Stupid name;
729     my $regex = shift;
730
731     if ($result =~ m@(.*?)($regex)(.*)@) {
732         $result = $1 . h('host') . $2 . h('Standard') . $3;
733     }
734
735     return $result;
736 }
737
738 sub higlight_matched_pattern ($$$) {
739
740     our %h;
741     my $result = shift; # XXX: Stupid name;
742     my $key = shift;
743     my $regex = shift;
744
745     die "Unknown key $key" unless defined $h{$key};
746
747     if ($result =~ m@(.*?)($regex)(.*)@) {
748         $result = $1 . h($key) . $2 . h('Standard') . $3;
749     }
750
751     return $result;
752 }
753
754
755 sub higlight_matched_path ($$) {
756
757     my $result = shift; # XXX: Stupid name;
758     my $regex = shift;
759
760     if ($result =~ m@(.*?)($regex)(.*)@) {
761         $result = $1 . h('path') . $2 . h('Standard') . $3;
762     }
763
764     return $result;
765 }
766
767
768 sub highlight_url ($) {
769
770     my $url = shift;
771
772     if (cli_option_is_set('html-output')) {
773
774         $url = '<a href="' . $url . '">' . $url . '</a>';
775
776     } else {
777
778         $url = h('URL') . $url . h('Standard');
779
780     }
781
782     return $url;
783 }
784
785 ################################################################################
786 # loglevel-specific highlighter functions
787 ################################################################################
788
789 sub handle_loglevel_header ($) {
790
791     my $content = shift;
792     my $c = $content;
793     our $t;
794     our %req;
795     our %h;
796     our %header_colours;
797     our @all_colours;
798     our $header_colour_index;
799     our $no_special_header_highlighting;
800
801     # Register new headers
802     # scan: Accept: image/png,image/*;q=0.8,*/*;q=0.5
803     if ($c =~ m/^scan: ((?>[^:]+)):/) {
804         my $header = $1;
805         if (!defined($header_colours{$header})) {
806             debug_message "Registering previously unknown header $1" if DEBUG_HEADER_REGISTERING;
807
808             if (REGISTER_HEADERS_WITH_THE_SAME_COLOUR) {
809                 $header_colours{$header} =  $header_colours{'Default'};
810             } else {
811                 $header_colours{$header} = $all_colours[$header_colour_index % @all_colours];
812                 $header_colour_index++;
813             }
814         }
815     }
816
817     if ($c =~ m/^scan: ((\w*) (.*) (HTTP\/\d\.\d))/) {
818
819             # Client request line
820             # Save for statistics (XXX: Not implemented yet)
821             $req{$t}{'method'} = $2;
822             $req{$t}{'destination'} = $3;
823             $req{$t}{'http-version'} = $4;
824
825             $content = highlight_request_line($1);
826
827     } elsif ($c =~ m/^(scan: )(HTTP\/\d\.\d (\d+) (.*))/) {
828
829             # Server response line
830             $req{$t}{'response_line'} = $2;
831             $req{$t}{'status_code'} = $3;
832             $req{$t}{'status_message'} = $4;
833             $content = $1 . highlight_response_line($req{$t}{'response_line'});
834
835     } elsif ($c =~ m/^Crunching (?:server|client) header: .* \(contains: ([^\)]*)\)/) {
836
837         # Crunching server header: Set-Cookie: trac_form_token=d5308c34e16d15e9e301a456; (contains: Cookie:)
838         $content =~ s@(?<=contains: )($1)@$h{'crunch-pattern'}$1$h{'Standard'}@;
839         $content =~ s@(Crunching)@$h{$1}$1$h{'Standard'}@;    
840
841     } elsif ($c =~ m/^New host is: ([^\s]*)\./) {
842
843         # New host is: trac.vidalia-project.net. Crunching Referer: http://www.vidalia-project.net/
844         $c = higlight_matched_host($c, '(?<=New host is: )[^\s]+');
845         $content = higlight_matched_url($c, '(?<=Crunching Referer: )[^\s]+');
846
847     } elsif ($c =~ m/^Text mode enabled by force. (Take cover)!/) {
848
849         # Text mode enabled by force. Take cover!
850         $content =~ s@($1)@$h{'warning'}$1$h{'Standard'}@;
851
852     } elsif ($c =~ m/^(New HTTP Request-Line: )(.*)/) {
853
854         # New HTTP Request-Line: GET http://www.privoxy.org/ HTTP/1.1
855         $content = $1 . highlight_request_line($2);
856
857     } elsif ($c =~ m/^Adjust(ed)? Content-Length to \d+/) {
858
859         # Adjusted Content-Length to 2132
860         # Adjust Content-Length to 33533
861         $content =~ s@(?<=Content-Length to )(\d+)@$h{'Number'}$1$h{'Standard'}@;
862         $content = higlight_known_headers($content);
863
864     } elsif ($c =~ m/^Destination extracted from "Host:" header. New request URL:/) {
865
866         # Destination extracted from "Host:" header. New request URL: http://www.cccmz.de/~ridcully/blog/
867         $content = higlight_matched_url($content, '(?<=New request URL: ).*');
868
869     } elsif ($c =~ m/^Couldn\'t parse:/) {
870
871         # XXX: These should probable be logged with LOG_LEVEL_ERROR
872         # Couldn't parse: If-Modified-Since: Wed, 21 Mar 2007 16:34:50 GMT (crunching!)
873         # Couldn't parse: at, 24 Mar 2007 13:46:21 GMT in If-Modified-Since: Sat, 24 Mar 2007 13:46:21 GMT (crunching!)
874         $content =~ s@^(Couldn\'t parse)@$h{'error'}$1$h{'Standard'}@;
875
876     } elsif ($c =~ /^Tagger \'([^\']*)\' added tag \'([^\']*)\'/ or
877              $c =~ m/^Adding tag \'([^\']*)\' created by header tagger \'([^\']*)\'/) {
878
879         # Adding tag 'GET request' created by header tagger 'method-man' (XXX: no longer used)
880         # Tagger 'revalidation' added tag 'REVALIDATION-REQUEST'. No action bit update necessary.
881         # Tagger 'revalidation' added tag 'REVALIDATION-REQUEST'. Action bits updated accordingly.
882
883         # XXX: Save tag and tagger
884
885         $content =~ s@(?<=^Tagger \')([^\']*)@$h{'tagger'}$1$h{'Standard'}@;
886         $content =~ s@(?<=added tag \')([^\']*)@$h{'tag'}$1$h{'Standard'}@;
887         $content =~ s@(?<=Action bits )(updated)@$h{'action-bits-update'}$1$h{'Standard'}@;
888         $no_special_header_highlighting = 1;
889
890     } elsif ($c =~ /^Tagger \'([^\']*)\' didn['']t add tag \'([^\']*)\'/) {
891
892         # Tagger 'revalidation' didn't add tag 'REVALIDATION-REQUEST'. Tag already present
893         # XXX: Save tag and tagger
894
895         $content =~ s@(?<=^Tagger \')([^\']*)@$h{'tag'}$1$h{'Standard'}@;
896         $content =~ s@(?<=didn['']t add tag \')([^\']*)@$h{'tagger'}$1$h{'Standard'}@;
897
898     } elsif ($c =~ m/^(?:scan:|Randomiz|addh:|Adding:|Removing:|Referer:|Modified:|Accept-Language header|[Cc]ookie)/
899           or $c =~ m/^(Text mode is already enabled|Denied request with NULL byte|Replaced:|add-unique:)/
900           or $c =~ m/^(Crunched (incoming|outgoing) cookie|Suppressed offer|Accepted the client)/
901           or $c =~ m/^(addh-unique|Referer forged to)/
902           or $c =~ m/^Downgraded answer to HTTP\/1.0/
903           or $c =~ m/^Parameter: \+hide-referrer\{[^\}]*\} is a bad idea, but I don\'t care./
904           or $c =~ m/^Referer (?:overwritten|replaced) with: Referer: / #XXX: should this be highlighted?
905           or $c =~ m/^Referer crunched!/
906           or $c =~ m/^crunched x-forwarded-for!/
907           or $c =~ m/^crunched From!/
908           or $c =~ m/^ modified$/
909           or $c =~ m/^Content filtering is enabled. Crunching:/
910           or $c =~ m/^force-text-mode overruled the client/
911           or $c =~ m/^Server time in the future\./
912           or $c =~ m/^content-disposition header crunched and replaced with:/i
913           or $c =~ m/^Reducing white space in /
914           or $c =~ m/^Ignoring single quote in /
915           or $c =~ m/^Converting tab to space in /
916             )
917     {
918         # XXX: Some of these may need highlighting
919
920         # Modified: User-Agent: Mozilla/5.0 (X11; U; SunOS i86pc; pl-PL; rv:1.8.1.1) Gecko/20070214 Firefox/2.0.0.1
921         # Accept-Language header crunched and replaced with: Accept-Language: pl-pl
922         # cookie 'Set-Cookie: eZSessionCookie=07bfec287c197440d299f81580593c3d; expires=Thursday, 12-Apr-07 15:16:18 GMT; path=/' send by http://wirres.net/article/articleview/4265/1/6/ appears to be using time format 1 (XXX: gone with the wind)
923         # Cookie rewritten to a temporary one: Set-Cookie: NSC_gffe-iuuq-mc-wtfswfs=8efb33a53660;path=/
924         # Text mode is already enabled
925         # Denied request with NULL byte(s) turned into line break(s)
926         # Replaced: 'Connection: Yo, home to Bel Air' with 'Connection: close'
927         # addh-unique: Host: people.freebsd.org
928         # Suppressed offer to compress content
929         # Crunched incoming cookie -- yum!
930         # Accepted the client's request to fetch without filtering.
931         # Crunched outgoing cookie: Cookie: PREF=ID=6cf0abd347b30262:TM=1173357617:LM=1173357617:S=jZypyyJ7LPiwFi1_
932         # addh-unique: Host: subkeys.pgp.net:11371
933         # Referer forged to: Referer: http://10.0.0.1/
934         # Downgraded answer to HTTP/1.0
935         # Parameter: +hide-referrer{pille-palle} is a bad idea, but I don't care.
936         # Referer overwritten with: Referer: pille-palle
937         # Referer replaced with: Referer: pille-palle
938         # crunched x-forwarded-for!
939         # crunched From!
940         #  modified # XXX: pretty stupid log message
941         # Content filtering is enabled. Crunching: 'Range: 1234-5678' to prevent range-mismatch problems
942         # force-text-mode overruled the client's request to fetch without filtering!
943         # Server time in the future.
944         # content-disposition header crunched and replaced with: content-disposition: filename=baz
945         # Content-Disposition header crunched and replaced with: content-disposition: filename=baz
946         # Reducing white space in 'X-LWS-Test: "This  is  quoted" this is not "this  is  " but " this again   is  not'
947         # Ignoring single quote in 'X-LWS-Test: "This  is  quoted" this is not "this  is  " but "  this again   is  not'
948         # Converting tab to space in 'X-LWS-Test:   "This  is  quoted" this   is  not "this  is  "  but  "  this again   is  not'
949
950     } elsif ($c =~ m/^scanning headers for:/) {
951
952         return '' unless SHOW_SCAN_INTRO;
953
954     } elsif ($c =~ m/^[Cc]runch(ing|ed)|crumble crunched:/) {
955         # crunched User-Agent!
956         # Crunching: Content-Encoding: gzip
957
958         $content =~ s@(Crunching|crunched)@$h{$1}$1$h{'Standard'}@;
959
960     } elsif ($c =~ m/^Offending request data with NULL bytes turned into \'°\' characters:/) {
961         
962         # Offending request data with NULL bytes turned into '°' characters: Â°Â°n°°(°°°
963
964         $content = h('warning') . $content . h('Standard');
965  
966     } elsif ($c =~ m/^(Transforming \")(.*?)(\" to \")(.*?)(\")/) {
967
968         # Transforming "Proxy-Authenticate: Basic realm="Correos Proxy Server"" to "Proxy-Authenticate: Basic realm="Correos Proxy Server""
969
970        $content =~ s@(?<=^Transforming \")(.*)(?=\" to)@$h{'Header'}$1$h{'Standard'}@;
971        $content =~ s@(?<=to \")(.*)(?=\")@$h{'Header'}$1$h{'Standard'}@;
972
973     } elsif ($c =~ m/^Removing empty header/) {
974
975         # Removing empty header
976         # Ignore for now
977
978     } elsif ($c =~ m/^Content-Type: .* not replaced/) {
979
980         # Content-Type: application/octet-stream not replaced. It doesn't look like text. Enable force-text-mode if you know what you're doing.
981         # XXX: Could highlight more here.
982         $content =~ s@(?<=^Content-Type: )(.*)(?= not replaced)@$h{'content-type'}$1$h{'Standard'}@;
983
984     } else {
985
986         found_unknown_content($content);
987     }
988
989     # Highlight headers   
990     unless ($c =~ m/^Transforming/) {
991         $content = higlight_known_headers($content) unless $no_special_header_highlighting;
992     }
993
994     return $content;
995 }
996
997 sub handle_loglevel_re_filter ($) {
998
999     my $content = shift;
1000     my $c = $content;
1001     my $key;
1002     our $t;
1003     our %req;
1004     our %h;
1005     our %header_colours;
1006     our @all_colours;
1007     our $header_colour_index;
1008
1009     if ($c =~ /\.{3}$/
1010         and $c =~ m/^(?:re_)?filtering \'?(.*?)\'? \(size (\d*)\) with (?:filter )?\'?([^\s]*?)\'? ?\.{3}$/) {
1011
1012         # Used by Privoxy 3.0.5 and 3.0.6:
1013         # XXX: Fill in ...
1014         # Used by Privoxy 3.0.7:
1015         # filtering 'Connection: close' (size 17) with 'generic-content-ads' ...
1016
1017         $req{$t}{'filtered_header'} = $1;
1018         $req{$t}{'old_header_size'} = $2;
1019         $req{$t}{'header_filter_name'} = $3;
1020
1021         unless (cli_option_is_set('show-ineffective-filters') or
1022                 $req{$t}{'header_filter_name'} =~ m/^privoxy-filter-test$/) {
1023             return '';
1024         }
1025         $content =~ s@(?<=\(size )(\d+)@$h{'Number'}$1$h{'Standard'}@;   
1026         $content =~ s@($req{$t}{'header_filter_name'})@$h{'filter'}$1$h{'Standard'}@;
1027
1028     } elsif ($c =~ m/^ ?\.\.\. ?produced (\d*) hits \(new size (\d*)\)\./) {
1029
1030         # ...produced 0 hits (new size 23).
1031         #... produced 1 hits (new size 54).
1032
1033         $req{$t}{'header_filter_hits'} = $1;
1034         $req{$t}{'new_header_size'} = $2;
1035
1036         unless (cli_option_is_set('show-ineffective-filters') or
1037                 (defined($req{$t}{'header_filter_name'}) and
1038                  $req{$t}{'header_filter_name'} =~ m/^privoxy-filter-test$/)) {
1039
1040             if ($req{$t}{'header_filter_hits'} == 0 and
1041                 not (defined($req{$t}{'header_filter_name'}) and
1042                  $req{$t}{'header_filter_name'} =~ m/^privoxy-filter-test$/)) {
1043                 return ''; 
1044             }
1045             # Reformat including information from the intro
1046             $c = "'" . h('filter') . $req{$t}{'header_filter_name'} . h('Standard') . "'";
1047             $c .= " hit ";
1048             # XXX: Hide behind constant, it may be interesting if LOG_LEVEL_HEADER isn't enabled as well.
1049             # $c .= $req{$t}{'filtered_header'} . " ";
1050             $c .= h('Number') . $req{$t}{'header_filter_hits'}. h('Standard');
1051             $c .= ($req{$t}{'header_filter_hits'} == 1) ? " time, " : " times, ";
1052
1053             if ($req{$t}{'old_header_size'} !=  $req{$t}{'new_header_size'}) {
1054
1055                 $c .= "changing size from ";
1056                 $c .=  h('Number') . $req{$t}{'old_header_size'} . h('Standard');
1057                 $c .= " to ";
1058                 $c .= h('Number') . $req{$t}{'new_header_size'} . h('Standard');
1059                 $c .= ".";
1060
1061             } else {
1062
1063                 $c .= "keeping the size at " . $req{$t}{'old_header_size'};
1064
1065             }
1066
1067             # Highlight from last line (XXX: What?)
1068             # $c =~ s@(?<=produced )(\d+)@$h{'Number'}$1$h{'Standard'}@;   
1069             # $c =~ s@($req{$t}{'header_filter_name'})@$h{'filter'}$1$h{'Standard'}@;
1070
1071         } else {
1072
1073            # XXX: Untested
1074            $c =~ s@(?<=produced )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1075            $c =~ s@(?<=new size )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1076
1077         }
1078         $content = $c;
1079
1080     } elsif ($c =~ m/^(Tagger|Filter) ([^\s]*) has empty joblist. Nothing to do./) {
1081
1082         # Filter privoxy-filter-test has empty joblist. Nothing to do.
1083         # Tagger variable-test has empty joblist. Nothing to do.
1084
1085         $content =~ s@(?<=$1 )([^\s]*)@$h{'filter'}$1$h{'Standard'}@;
1086
1087     } elsif ($c =~ m/^(?:re_)?filtering ([^\s]+) \(size (\d+)\) with (?:filter )?\'?([^\s]+?)\'? produced (\d+) hits \(new size (\d+)\)/) {
1088
1089         # XXX: only the second version gets highlighted properly.
1090         # re_filtering www.lfk.de/favicon.ico (size 209) with filter untrackable-hulk produced 0 hits (new size 209).
1091         # filtering aci.blogg.de/ (size 37988) with 'blogg.de' produced 3 hits (new size 38057)
1092         $req{$t}{'content_source'} = $1;
1093         $req{$t}{'content_size'}   = $2;
1094         $req{$t}{'content_filter'} = $3;
1095         $req{$t}{'content_hits'}   = $4;
1096         $req{$t}{'new_content_size'} = $5;
1097         $req{$t}{'content_size_change'} = $req{$t}{'new_content_size'} - $req{$t}{'content_size'};
1098         #return '' if ($req{$t}{'content_hits'} == 0 && !cli_option_is_set('show-ineffective-filters'));
1099         if ($req{$t}{'content_hits'} == 0 and
1100             not (cli_option_is_set('show-ineffective-filters')
1101                  or ($req{$t}{'content_filter'} =~ m/^privoxy-filter-test$/))) {
1102                 return ''; 
1103         }
1104
1105         $c =~ s@(?<=\(size )(\d+)\)(?= with)@$h{'Number'}$1$h{'Standard'}@;
1106         $c =~ s@(?<=\(new size )(\d+)@$h{'Number'}$1$h{'Standard'}@;
1107         $c =~ s@(?<=produced )(\d+)(?= hits)@$h{'Number'}$1$h{'Standard'}@;
1108
1109         $c =~ s@([^\s]+?)(\'? produced)@$h{'filter'}$1$h{'Standard'}$2@;
1110         $c = higlight_matched_host($c, '(?<=filtering )[^\s]+');
1111
1112         $c =~ s@\.$@ @;
1113         $c .= "(" . $h{'Number'};
1114         $c .= "+" if ($req{$t}{'content_size_change'} >= 0);
1115         $c .= $req{$t}{'content_size_change'} . $h{'Standard'} . ")";
1116         $content = $c;
1117
1118     } elsif ($c =~ m/^De-chunking successful. Shrunk from (\d+) to (\d+)/) {
1119
1120         $req{$t}{'chunked-size'} = $1;
1121         $req{$t}{'dechunked-size'} = $2;
1122         $req{$t}{'dechunk-change'} = $req{$t}{'dechunked-size'} - $req{$t}{'chunked-size'};
1123
1124         $content .= " (" . h('Number') . $req{$t}{'dechunk-change'} . h('Standard') . ")";
1125
1126         $content =~ s@(?<=from )($req{$t}{'chunked-size'})@$h{'Number'}$1$h{'Standard'}@;
1127         $content =~ s@(?<=to )($req{$t}{'dechunked-size'})@$h{'Number'}$1$h{'Standard'}@;
1128
1129     } elsif ($c =~ m/^Decompression successful. Old size: (\d+), new size: (\d+)./) {
1130
1131         # Decompression successful. Old size: 670, new size: 1166.
1132
1133         $req{$t}{'size-compressed'} = $1;
1134         $req{$t}{'size-decompressed'} = $2;
1135         $req{$t}{'decompression-gain'} = $req{$t}{'size-decompressed'} - $req{$t}{'size-compressed'};
1136
1137         $content =~ s@(?<=Old size: )($req{$t}{'size-compressed'})@$h{'Number'}$1$h{'Standard'}@;
1138         $content =~ s@(?<=new size: )($req{$t}{'size-decompressed'})@$h{'Number'}$1$h{'Standard'}@;
1139
1140         # XXX: Create sub get_percentage()
1141         if ($req{$t}{'size-decompressed'}) {
1142             $req{$t}{'decompression-gain-percent'} =
1143                 $req{$t}{'decompression-gain'} / $req{$t}{'size-decompressed'} * 100;
1144
1145             $content .= " (saved: ";
1146             #$content .= h('Number') . $req{$t}{'decompression-gain'} . h('Standard');
1147             #$content .= "/";
1148             $content .= h('Number') . sprintf("%.2f%%", $req{$t}{'decompression-gain-percent'}) . h('Standard');
1149             $content .= ")";
1150         }
1151
1152     } elsif ($c =~ m/^(Need to de-chunk first)/) {
1153
1154         # Need to de-chunk first
1155         return '' if SUPPRESS_NEED_TO_DE_CHUNK_FIRST;
1156
1157     } elsif ($c =~ m/^(Adding (?:dynamic )?re_filter job)/) {
1158
1159         return ''  if (SUPPRESS_SUCCEEDED_FILTER_ADDITIONS && m/succeeded/);
1160
1161         # Adding re_filter job ...
1162         # Adding dynamic re_filter job s@^(?:\w*)\s+.*\s+HTTP/\d\.\d\s*@IP-ADDRESS: $origin@D to filter client-ip-address succeeded.
1163
1164     } elsif ($c =~ m/^Reading in filter/) {
1165
1166         return '' unless SHOW_FILTER_READIN_IN;
1167
1168     } else {
1169
1170         found_unknown_content($content);
1171
1172     }
1173
1174     return $content;
1175 }
1176
1177
1178 sub handle_loglevel_redirect ($) {
1179
1180     my $c = shift;
1181     our $t;
1182     our %req;
1183     our %h;
1184
1185     if ($c =~ m/^Decoding "([^""]*)"/) {
1186
1187          $req{$t}{'original-destination'} = $1;
1188          $c = higlight_matched_path($c, '(?<=Decoding ")[^"]*');
1189          $c =~ s@\"@@g;
1190
1191     } elsif ($c =~ m/^Checking/) {
1192
1193          # Checking /_ylt=A0geu.Z76BRGR9kAH2RXNyoA/SIG=14gqhtscv/EXP=1175861755/**http://search.yahoo.com/search?p=view+odb+presentation+on+freebsd&ei=UTF-8&xargs=0&pstart=1&fr=moz2&b=11 for redirects.
1194
1195          # TODO: Change colour if really url-decoded
1196          $req{$t}{'decoded-original-destination'} = $1;
1197          $c = higlight_matched_path($c, '(?<=Checking ")[^"]*');
1198          $c =~ s@\"@@g;
1199
1200     } elsif ($c =~ m/^pcrs command "([^""]*)" changed "([^""]*)" to "([^""]*)" \((\d+) hits?\)/) {
1201
1202         # pcrs command "s@&from=rss@@" changed "http://it.slashdot.org/article.pl?sid=07/03/02/1657247&from=rss" to "http://it.slashdot.org/article.pl?sid=07/03/02/1657247" (1 hit).
1203
1204         my ($pcrs_command, $url_before, $url_after, $hits) = ($1, $2, $3, $4); # XXX: save these?
1205
1206         $c =~ s@(?<=pcrs command )"([^""]*)"@$h{'filter'}$1$h{'Standard'}@;
1207         $c = higlight_matched_url($c, '(?<=changed ")[^""]*');
1208         $c =~ s@(?<=changed )"([^""]*)"@$1@; # Remove quotes
1209         $c = higlight_matched_url($c, '(?<=to ")[^""]*');
1210         $c =~ s@(?<=to )"([^""]*)"@$1@; # Remove quotes
1211         $c =~ s@(\d+)(?= hits?)@$h{'hits'}$1$h{'Standard'}@;
1212
1213     } elsif ($c =~ m/(^New URL is: )(.*)/) {
1214
1215         # New URL is: http://it.slashdot.org/article.pl?sid=07/03/04/1511210
1216         # XXX: Use URL highlighter
1217         # XXX: Save?
1218         $c = $1 . h('rewritten-URL') . $2 . h('Standard');
1219
1220     } elsif ($c =~ m/No pcrs command recognized, assuming that/) {
1221         # No pcrs command recognized, assuming that "http://config.privoxy.org/user-manual/favicon.png" is already properly formatted.
1222         # XXX: assume the same?
1223         $c = higlight_matched_url($c, '(?<=assuming that \")[^"]*');
1224
1225     } else {
1226
1227         found_unknown_content($c);
1228
1229     }
1230
1231     return $c;
1232 }
1233
1234 sub handle_loglevel_gif_deanimate ($) {
1235
1236     my $content = shift;
1237     our $t;
1238     our %req;
1239     our %h;
1240
1241     if ($content =~ m/Success! GIF shrunk from (\d+) bytes to (\d+)\./) {
1242
1243         my $bytes_from = $1;
1244         my $bytes_to = $2;
1245         # Gif-Deanimate: Success! GIF shrunk from 205 bytes to 133.
1246         $content =~ s@$bytes_from@$h{'Number'}$bytes_from$h{'Standard'}@;
1247         # XXX: Do we need g in case of ($1 == $2)?
1248         $content =~ s@$bytes_to@$h{'Number'}$bytes_to$h{'Standard'}@;
1249
1250     } elsif ($content =~ m/GIF (not) changed/) {
1251
1252         # Gif-Deanimate: GIF not changed.
1253         return '' if SUPPRESS_GIF_NOT_CHANGED;
1254         $content =~ s@($1)@$h{'not'}$1$h{'Standard'}@;
1255
1256     } elsif ($content =~ m/^failed! \(gif parsing\)/) {
1257
1258         # failed! (gif parsing)
1259         # XXX: Replace this error message with something less stupid 
1260         $content =~ s@(failed!)@$h{'error'}$1$h{'Standard'}@;
1261
1262     } elsif ($content =~ m/^Need to de-chunk first/) {
1263
1264         # Need to de-chunk first
1265         return '' if SUPPRESS_NEED_TO_DE_CHUNK_FIRST;
1266
1267     } elsif ($content =~ m/^(?:No GIF header found|failed while parsing)/) {
1268
1269         # No GIF header found (XXX: Did I ever commit this?)
1270         # failed while parsing 195 134747048 (XXX: never commited)
1271
1272         # Ignore these for now
1273
1274     } else {
1275
1276         found_unknown_content($content);
1277
1278     }
1279
1280     return $content;
1281 }
1282
1283
1284 sub handle_loglevel_request ($) {
1285
1286     my $content = shift;
1287     our $t;
1288     our %req;
1289     our %h;
1290     our %reason_colours;
1291
1292     if ($content =~ m/crunch! /) {
1293
1294         # config.privoxy.org/send-stylesheet crunch! (CGI Call)
1295
1296         # Highlight crunch reasons
1297         foreach my $reason (keys %reason_colours) {
1298             $content =~ s@\(($reason)\)@$reason_colours{$reason}($1)$h{'Standard'}@g;
1299         }
1300         # Highlight request URL domain and ditch 'crunch!'
1301         $content = higlight_matched_pattern($content, 'request_', '[^ ]*(?= crunch!)');
1302         $content =~ s@ crunch!@@;
1303
1304     } elsif ($content =~ m/\[too long, truncated\]$/) {
1305
1306         # config.privoxy.org/edit-actions-submit?f=3&v=1176116716&s=7&Submit=Submit[...]&filter... [too long, truncated]
1307         $content = higlight_matched_pattern($content, 'request_', '^.*(?=\.\.\. \[too long, truncated\]$)');
1308
1309     } elsif ($content =~ m/(.*)/) { # XXX: Pretty stupid
1310
1311         # trac.vidalia-project.net/wiki/Volunteer?format=txt
1312         $content = h('request_') . $content . h('Standard');
1313
1314     } else {  # XXX: Nop
1315
1316         found_unknown_content($content);
1317
1318     }
1319             
1320     return $content;
1321 }
1322
1323 sub handle_loglevel_connect ($) {
1324
1325     my $c = shift;
1326     our $t;
1327     our %req;
1328     our %h;
1329
1330     if ($c =~ m/via [^\s]+ to: [^\s]+/) {
1331
1332         # Connect: via 10.0.0.1:8123 to: www.example.org.noconnect
1333
1334         $c = higlight_matched_host($c, '(?<=via )[^\s]+');
1335         $c = higlight_matched_host($c, '(?<=to: )[^\s]+');
1336
1337     } elsif ($c =~ m/connect to: .* failed: .*/) {
1338
1339         # connect to: www.example.org.noconnect failed: Operation not permitted
1340
1341         $c = higlight_matched_host($c, '(?<=connect to: )[^\s]+');
1342
1343         $c =~ s@(?<=failed: )(.*)@$h{'error'}$1$h{'Standard'}@;
1344
1345     } elsif ($c =~ m/to ([^\s]*) successful$/) {
1346
1347         # Connect: to www.nzherald.co.nz successful
1348
1349         return '' if SUPPRESS_SUCCESSFUL_CONNECTIONS;
1350         $c = higlight_matched_host($c, '(?<=to )[^\s]+');
1351
1352     } elsif ($c =~ m/to ([^\s]*)$/) {
1353
1354         # Connect: to lists.sourceforge.net:443
1355
1356         $c = higlight_matched_host($c, '(?<=to )[^\s]+');
1357
1358     } elsif ($c =~ m/^accepted connection from .*/ or
1359              $c =~ m/^OK/) {
1360
1361         # accepted connection from 10.0.0.1
1362         # Privoxy 3.0.6 and earlier just say:
1363         # OK
1364         return '' if SUPPRESS_ACCEPTED_CONNECTIONS;
1365         $c = higlight_matched_host($c, '(?<=connection from ).*');
1366
1367     } elsif ($c =~ m/^write header to: .* failed:/) {
1368
1369         # write header to: 10.0.0.1 failed: Broken pipe
1370
1371         $c = higlight_matched_host($c, '(?<=write header to: )[^\s]*');
1372         $c =~ s@(?<=failed: )(.*)@$h{'Error'}$1$h{'Standard'}@;
1373
1374     } elsif ($c =~ m/^write header to client failed:/) {
1375
1376         # write header to client failed: Broken pipe
1377         # XXX: Stil in use?
1378         $c =~ s@(?<=failed: )(.*)@$h{'Error'}$1$h{'Standard'}@;
1379
1380     } elsif ($c =~ m/^socks4_connect:/) {
1381
1382         # socks4_connect: SOCKS request rejected or failed.
1383         $c =~ s@(?<=socks4_connect: )(.*)@$h{'Error'}$1$h{'Standard'}@;
1384
1385     } elsif ($c =~ m/^Listening for new connections/ or
1386              $c =~ m/^accept connection/) {
1387         # XXX: Highlight?
1388         # Privoxy versions above 3.0.6 say:
1389         # Listening for new connections ...
1390         # earlier versions say:
1391         # accept connection ...
1392         return '';
1393
1394     } elsif ($c =~ m/^accept failed:/) {
1395
1396         $c =~ s@(?<=accept failed: )(.*)@$h{'Error'}$1$h{'Standard'}@;
1397
1398     } elsif ($c =~ m/^Overriding forwarding settings/) {
1399
1400         # Overriding forwarding settings based on 'forward 10.0.0.1:8123'
1401         $c =~ s@(?<=based on \')(.*)(?=\')@$h{'configuration-line'}$1$h{'Standard'}@;
1402
1403     } elsif ($c =~ m/^Denying suspicious CONNECT request from/) {
1404
1405         # Denying suspicious CONNECT request from 10.0.0.1
1406         $c = higlight_matched_host($c, '(?<=from )[^\s]+'); # XXX: not an URL
1407
1408     } elsif ($c =~ m/^socks5_connect:/) {
1409     
1410         $c =~ s@(?<=socks5_connect: )(.*)@$h{'error'}$1$h{'Standard'}@;
1411
1412     } else {
1413
1414         found_unknown_content($c);
1415
1416     }
1417             
1418     return $c;
1419 }
1420
1421
1422 sub handle_loglevel_info ($) {
1423
1424     my $c = shift;
1425     our $t;
1426     our %req;
1427     our %h;
1428  
1429     if ($c =~ m/^Rewrite detected:/) {
1430
1431         # Rewrite detected: GET http://10.0.0.2:88/blah.txt HTTP/1.1
1432         $c = higlight_matched_request_line($c, '(?<=^Rewrite detected: ).*');
1433
1434     } elsif ($c =~ m/^Decompress(ing deflated|ion didn)/ or
1435              $c =~ m/^Compressed content detected/ or
1436              $c =~ m/^Tagger/
1437             ) {
1438         # Decompressing deflated iob: 117
1439         # Decompression didn't result in any content.
1440         # Compressed content detected, content filtering disabled. Consider recompiling Privoxy with zlib support or enable the prevent-compression action.
1441         # Tagger 'complete-url' created empty tag. Ignored.
1442
1443         # Ignored for now
1444
1445     } elsif ($c =~ m/^(Re)?loading configuration file /) {
1446
1447         # loading configuration file '/usr/local/etc/privoxy/config':
1448         # Reloading configuration file '/usr/local/etc/privoxy/config'
1449         $c =~ s@(?<=loading configuration file \')([^\']*)@$h{'file'}$1$h{'Standard'}@;
1450
1451     } elsif ($c =~ m/^exiting by signal/) {
1452         
1453         # exiting by signal 15 .. bye
1454         $c =~ s@(?<=exiting by signal )(\d+)@$h{'signal'}$1$h{'Standard'}@;
1455
1456     } elsif ($c =~ m/^Privoxy version/) {
1457         
1458         # Privoxy version 3.0.7
1459         $c =~ s@(?<=^Privoxy version )(\d+\.\d+\.\d+)$@$h{'version'}$1$h{'Standard'}@;
1460
1461     } elsif ($c =~ m/^Program name: /) {
1462
1463         # Program name: /usr/local/sbin/privoxy
1464         $c =~ s@(?<=Program name: )(.*)@$h{'program-name'}$1$h{'Standard'}@;
1465
1466     } elsif ($c =~ m/^Listening on port /) {
1467
1468         # Listening on port 8118 on IP address 10.0.0.1
1469         $c =~ s@(?<=Listening on port )(\d+)@$h{'port'}$1$h{'Standard'}@;
1470         $c =~ s@(?<=on IP address )(.*)@$h{'ip-address'}$1$h{'Standard'}@;
1471
1472     } elsif ($c =~ m/^\(Re-\)Open(?:ing)? logfile/) {
1473
1474         # (Re-)Open logfile /var/log/privoxy/privoxy.log
1475         $c =~ s@(?<=Open logfile )(.*)@$h{'file'}$1$h{'Standard'}@;
1476
1477     } elsif ($c =~ m/^(Request from|Malformed server response detected)/) {
1478
1479         # Request from 10.0.0.1 denied. limit-connect{,} doesn't allow CONNECT requests to port 443.
1480         # Request from 10.0.0.1 marked for blocking. limit-connect{,} doesn't allow CONNECT requests to port 443.
1481         # Malformed server response detected. Downgrading to HTTP/1.0 impossible.
1482
1483         $c =~ s@(?<=Request from )([^\s]*)@$h{'ip-address'}$1$h{'Standard'}@;
1484         $c =~ s@(denied|blocking)@$h{'warning'}$1$h{'Standard'}@;
1485         $c =~ s@(CONNECT)@$h{'method'}$1$h{'Standard'}@;
1486         $c =~ s@(?<=to port )(\d+)@$h{'port'}$1$h{'Standard'}@;
1487
1488     } elsif ($c =~ m/^No logfile configured/ or
1489              $c =~ m/^Malformerd HTTP headers detected and MS IIS5 hack enabled/ or
1490              $c =~ m/^Invalid \"chunked\" transfer/
1491              ) {
1492
1493         # No logfile configured. Please enable it before reporting any problems.
1494         # Malformerd HTTP headers detected and MS IIS5 hack enabled. Expect an invalid response or even no response at all.
1495         # No logfile configured. Logging disabled.
1496         # Invalid "chunked" transfer encoding detected and ignored.
1497
1498     } else {
1499
1500         found_unknown_content($c);
1501
1502     }
1503
1504     return $c;
1505 }
1506
1507 sub handle_loglevel_cgi ($) {
1508
1509     my $c = shift;
1510     our $t;
1511     our %req;
1512     our %h;
1513
1514     if ($c =~ m/^Granting access to/) {
1515       
1516         #Granting access to http://config.privoxy.org/send-stylesheet, referrer http://p.p/ is trustworthy.
1517
1518     } elsif ($c =~ m/^Substituting: s(.)/) {
1519       
1520         # Substituting: s/@else-not-FEATURE_ZLIB@.*@endif-FEATURE_ZLIB@//sigTU
1521         # XXX: prone to span several lines
1522
1523         my $delimiter = $1;
1524         #$c =~ s@(?<=failed: )(.*)@$h{'error'}$1$h{'Standard'}@;
1525         $c =~ s@(?!<=\\)($delimiter)@$h{'pcrs-delimiter'}$1$h{'Standard'}@g; # XXX: Too aggressive
1526         #$c =~ s@(?!<=\\)($1)@$h{'pcrs-delimiter'}$1$h{'Standard'}@g;
1527     }
1528
1529     return $c;
1530 }
1531
1532 sub handle_loglevel_force ($) {
1533
1534     my $c = shift;
1535     our $t;
1536     our %req;
1537     our %h;
1538
1539     if ($c =~ m/^Ignored force prefix in request:/) {
1540       
1541         # Ignored force prefix in request: "GET http://10.0.0.1/PRIVOXY-FORCE/block HTTP/1.1"
1542         $c =~ s@^(Ignored)@$h{'ignored'}$1$h{'Standard'}@;
1543         $c = higlight_matched_request_line($c, '(?<=request: ")[^"]*');
1544
1545     } elsif ($c =~ m/^Enforcing request:/) {
1546       
1547         # Enforcing request: "GET http://10.0.0.1/block HTTP/1.1".
1548         $c = higlight_matched_request_line($c, '(?<=request: ")[^"]*');
1549
1550     } else {
1551
1552         found_unknown_content($c);
1553
1554     }
1555
1556     return $c;
1557 }
1558
1559 sub handle_loglevel_ignore ($) {
1560     return shift;
1561 }
1562
1563 ################################################################################
1564 # Functions that actually print stuff
1565 ################################################################################
1566
1567 sub print_clf_message () {
1568
1569     our ($ip, $timestamp, $request_line, $status_code, $size);
1570     our %h;
1571     my $output = '';
1572
1573     return if DEBUG_SUPPRESS_LOG_MESSAGES;
1574
1575     # Rebuild highlighted
1576     $output .= $h{'Number'} . $ip . $h{'Standard'};
1577     $output .= " - - ";
1578     $output .= "[" . $h{'Timestamp'} . $timestamp . $h{'Standard'} . "]";
1579     $output .= " ";
1580     $output .= "\"" . highlight_request_line("$request_line") . "\"";
1581     $output .= " ";
1582     $output .= $h{'Status'} . $status_code . $h{'Standard'};
1583     $output .= " ";
1584     $output .= $h{'Number'} . $size . $h{'Standard'};
1585     $output .= get_line_end();
1586
1587     print $output;
1588 }
1589
1590 sub print_non_clf_message ($) {
1591
1592     our %req;
1593     our %thread_colours;
1594     our %h;
1595     our $t;
1596     our $time_colour_index;
1597     our @time_colours;
1598     my $output;
1599     my $content = shift;
1600     my ($day, $time_stamp, $msecs, $thread, $log_level)
1601      = ($req{$t}{'day'}, $req{$t}{'time-stamp'}, $req{$t}{'msecs'}, $t, $req{$t}{'log-level'} );
1602
1603     return if DEBUG_SUPPRESS_LOG_MESSAGES;
1604
1605     $output .= $h{"Standard"} unless cli_option_is_set('html-output');
1606     #    $output .= "$day ";
1607     $output .= $time_colours[$time_colour_index % 2]; 
1608
1609     $output .= $time_stamp;
1610     $output .= ".$msecs" unless cli_option_is_set('no-msecs');
1611     $output .= $h{"Standard"};
1612     $output .= " ";
1613     $output .= $thread_colours{$thread} if (defined($thread_colours{$thread}));
1614     $output .= $thread;
1615     $output .= $h{"Standard"} . " ";
1616     $output .= $h{$log_level} if (defined($h{$log_level}));
1617     $output .= $log_level;
1618     $output .= $h{"Standard"} . ": ";
1619     $output .= "$content";
1620     $output .= get_line_end();
1621
1622     print $output;
1623 }
1624
1625 sub parse_loop () {
1626
1627     our $t;
1628     our %req; # request data from previous lines
1629     our %h;
1630     our %thread_colours;
1631     our @all_colours;
1632     our @time_colours;
1633     our $thread_colour_index = 0;
1634     our $header_colour_index = 0;
1635     our $time_colour_index = 0;
1636
1637     my ($day, $time_stamp, $thread, $log_level, $content, $c, $msecs);
1638     my $last_msecs  = 0;
1639     my $last_thread = 0;
1640     my $last_timestamp = 0;
1641     my $output;
1642     my $filters_that_did_nothing;
1643     my $key;
1644     my $time_colour;
1645     our $no_special_header_highlighting;
1646     $time_colour = paint_it('white');
1647     my %log_level_count;
1648
1649     my %log_level_handlers = (
1650         'Re-Filter'     => \&handle_loglevel_re_filter,
1651         'Header'        => \&handle_loglevel_header,
1652         'Connect'       => \&handle_loglevel_connect,
1653         'Redirect'      => \&handle_loglevel_redirect,
1654         'Request'       => \&handle_loglevel_request,
1655         'Gif-Deanimate' => \&handle_loglevel_gif_deanimate,
1656         'Info'          => \&handle_loglevel_info,
1657         'CGI'           => \&handle_loglevel_cgi,
1658         'Force'         => \&handle_loglevel_force,
1659         'Error'         => \&handle_loglevel_ignore,
1660         'Fatal error'   => \&handle_loglevel_ignore,
1661         'Writing'       => \&handle_loglevel_ignore,
1662     );
1663
1664     while (<>) {
1665  
1666         $output = '';
1667
1668         if (m/^(\w{3} \d{2}) (\d\d:\d\d:\d\d)\.?(\d+)? (?:Privoxy\(([^\)]*)\)) ([\w -]*): (.*)$/) {
1669             # XXX: Put in req hash?
1670             $day = $1;
1671             $time_stamp = $2;
1672             $msecs = $3 ? $3 : 0; # Only the cool kids have micro second resolution
1673             $log_level = $5;
1674             $content = $c = $6;
1675             $thread = $t = $4;
1676
1677             $req{$t}{'day'} = $day;
1678             $req{$t}{'time-stamp'} = $time_stamp;
1679             $req{$t}{'msecs'} = $msecs; # Only the cool kids have micro second resolution;
1680             $req{$t}{'log-level'} = $log_level;
1681             $req{$t}{'content'} = $content;
1682             $req{$t}{'log-message'} = $_;
1683             $no_special_header_highlighting = 0;
1684
1685             $log_level_count{$log_level}++;
1686
1687             if (defined($log_level_handlers{$log_level})) {
1688
1689                 $content = $log_level_handlers{$log_level}($content);
1690
1691             } else {
1692
1693                 die "No handler found for log level \"$log_level\"\n";
1694
1695             }
1696
1697             # Highlight Truncations    
1698             if (m/\.\.\. \[(too long, truncated)/) {
1699                 $content =~ s@($1)@$h{'Truncation'}$1$h{'Standard'}@g;
1700             }
1701
1702             next unless $content;
1703
1704             # Register threads to keep the colour constant
1705             if (!defined($thread_colours{$thread})) {
1706                 $thread_colours{$thread} = $all_colours[$thread_colour_index % @all_colours];
1707                 $thread_colour_index++;
1708             }
1709
1710             # Switch timestamp colour if timestamps differ
1711             if ($msecs != $last_msecs || !($time_stamp =~ m/$last_timestamp/)) {
1712                debug_message("Tick tack!") if DEBUG_TICKS;
1713                $time_colour = $time_colours[$time_colour_index % 2]; 
1714                $time_colour_index++
1715             }
1716
1717             $last_msecs = $msecs;
1718             $last_thread = $thread;
1719             $last_timestamp = $time_stamp;
1720
1721             print_non_clf_message($content);
1722
1723         } elsif (m/^(\d+\.\d+\.\d+\.\d+) - - \[(.*)\] "(.*)" (\d+) (\d+)/) {
1724
1725             # LOG_LEVEL_CLF lines look like this
1726             # 61.152.239.32 - - [04/Mar/2007:18:28:23 +0100] "GET http://ad.yieldmanager.com/imp?z=1&Z=120x600&s=109339&u=http%3A%2F%2Fwww.365loan.co.uk%2F&r=1 HTTP/1.1" 403 1730
1727             our ($ip, $timestamp, $request_line, $status_code, $size) = ($1, $2, $3, $4, $5);
1728
1729             print_clf_message();
1730     
1731         } else {
1732
1733             # Some Privoxy log messages span more than one line,
1734             # usually to dump lots of content that doesn't need any syntax highlighting.
1735             # XXX: add mechanism to forward these lines to the right handler anyway.
1736             chomp();
1737             unless (DEBUG_SUPPRESS_LOG_MESSAGES or (SUPPRESS_EMPTY_LINES and m/^\s+$/)) {
1738                 print and print get_line_end(); # unless (SUPPRESS_EMPTY_LINES and m/^\s+$/);
1739             }
1740         }
1741     }
1742
1743     if (cli_option_is_set('statistic')) {
1744         foreach (keys %log_level_count) {
1745             print $_ . ": " . $log_level_count{$_} . " ";
1746         }
1747     }
1748 }
1749
1750 sub VersionMessage {
1751     my $version_message;
1752
1753     $version_message .= 'Privoxy-Log-Parser ' . PRIVOXY_LOG_PARSER_VERSION  . "\n";
1754     $version_message .= 'Copyright (C) 2007-2008 Fabian Keil <fk@fabiankeil.de>' . "\n";
1755     $version_message .= 'http://www.fabiankeil.de/sourcecode/privoxy-log-parser/' . "\n";
1756
1757     print $version_message;
1758 }
1759
1760 sub get_cli_options () {
1761
1762     our %cli_options = (
1763         'html-output'              => CLI_OPTION_DEFAULT_TO_HTML_OUTPUT,
1764         'title'                    => CLI_OPTION_TITLE,
1765         'no-syntax-highlighting'   => CLI_OPTION_NO_SYNTAX_HIGHLIGHTING,
1766         'no-embedded-css'          => CLI_OPTION_NO_EMBEDDED_CSS,
1767         'no-msecs'                 => CLI_OPTION_NO_MSECS,
1768         'show-ineffective-filters' => CLI_OPTION_SHOW_INEFFECTIVE_FILTERS,
1769         'accept-unknown-messages'  => CLI_OPTION_ACCEPT_UNKNOWN_MESSAGES,
1770         'statistic'                => CLI_OPTION_STATISTIC,
1771     ); 
1772
1773     GetOptions (
1774         'html-output'              => \$cli_options{'html-output'},
1775         'title'                    => \$cli_options{'title'},
1776         'no-syntax-highlighting'   => \$cli_options{'no-syntax-highlighting'},
1777         'no-embedded-css'          => \$cli_options{'no-embedded-css'},
1778         'no-msecs'                 => \$cli_options{'no-msecs'},
1779         'show-ineffective-filters' => \$cli_options{'show-ineffective-filters'},
1780         'accept-unknown-messages'  => \$cli_options{'accept-unknown-messages'},
1781         'statistic'                => \$cli_options{'statistic'},
1782         'version'                  => sub { VersionMessage && exit(0) }
1783    );
1784 }
1785
1786
1787
1788 ################################################################################
1789 # main
1790 ################################################################################
1791 sub main () {
1792
1793     get_cli_options();
1794     set_background(DEFAULT_BACKGROUND);
1795     prepare_our_stuff();
1796
1797     print_intro();
1798
1799     parse_loop();
1800
1801     print_outro();
1802 }
1803
1804 main();
1805
1806 =head1 NAME
1807
1808 B<privoxy-log-parser> - A parser and syntax-highlighter for Privoxy log messages
1809
1810 =head1 SYNOPSIS
1811
1812 B<privoxy-log-parser> [B<--accept-unknown-messages>] [B<--html-output>]
1813 [B<--no-msecs>] [B<--no-syntax-higlighting>] [B<--show-ineffective-filters>]
1814 [B<--version>]
1815
1816 =head1 DESCRIPTION
1817
1818 B<privoxy-log-parser> reads Privoxy log messages and
1819
1820 - syntax-highlights recognized lines,
1821
1822 - reformats some of them for easier comprehension,
1823
1824 - filters out less useful messages, and
1825
1826 - (in some cases) calculates additional information,
1827   like the compression ratio or how a filter affected
1828   the content size.
1829  
1830 With B<privoxy-log-parser> you should be able to increase Privoxy's log level
1831 without getting confused by the resulting amount of output. For example for
1832 "debug 64" B<privoxy-log-parser> will (by default) only show messages that
1833 affect the content. If a filter doesn't cause any hits, B<privoxy-log-parser>
1834 will hide the "filter foo caused 0 hits" message.
1835
1836 =head1 OPTIONS
1837
1838 [B<--accept-unknown-messages>] Don't print warnings in case of unknown messages,
1839 just don't highlight them.
1840
1841 [B<--html-output>] Use HTML and CSS for the syntax highlighting. If this option is
1842 omitted, ANSI escape sequences are used unless B<--no-syntax-highlighting> is active.
1843 This option is only intended to make embedding log excerpts in web pages easier.
1844 It does not excape any input!
1845
1846 [B<--no-msecs>] Don't expect milisecond resolution
1847
1848 [B<--no-syntax-highlighting>] Disable syntax-highlighting. Useful when
1849 the filtered output is piped into less in which case the ANSI control
1850 codes don't work, or if the terminal itself doesn't support the control
1851 codes.
1852
1853 [B<--show-ineffective-filters>] Don't suppress log lines for filters
1854 that didn't modify the content.
1855
1856 [B<--version>] Print version and exit.
1857
1858 =head1 EXAMPLES
1859
1860 To monitor a log file:
1861
1862 tail -F /usr/jails/privoxy-jail/var/log/privoxy/privoxy.log | B<privoxy-log-parser>
1863
1864 Replace '-F' with '-f' if your tail implementation lacks '-F' support
1865 or if the log won't get rotated anyway. The log file location depends
1866 on your system (Doh!).
1867
1868 To monitor Privoxy without having it write to a log file:
1869
1870 privoxy --no-daemon /usr/jails/privoxy-jail/usr/local/etc/privoxy/config 2>&1 | B<privoxy-log-parser>
1871
1872 Again, the config file location depends on your system. Output redirection
1873 depends on your shell, the above works with bourne shells.
1874
1875 To read a processed Privoxy log file from top to bottom, letting the content
1876 scroll by slightly faster than you can read:
1877
1878 B<privoxy-log-parser> < /usr/jails/privoxy-jail/var/log/privoxy/privoxy.log
1879
1880 This is probably only useful to fill screens in the background of haxor movies.
1881
1882 =head1 CAVEATS
1883
1884 Syntax highlighting with ANSI escape sequences will look strange
1885 if your background color isn't black.
1886
1887 Some messages aren't recognized yet and will not be fully highlighted.
1888
1889 B<privoxy-log-parser> is developed with Privoxy 3.0.7 or later in mind,
1890 using earlier Privoxy versions will probably result in an increased amount
1891 of unrecognized log lines.
1892
1893 Privoxy's log files tend to be rather large. If you use HTML
1894 highlighting some browsers can't handle them, get confused and
1895 will eventually crash because of segmentation faults or unexpected
1896 exceptions. This is a problem in the browser and not B<privoxy-log-parser>'s
1897 fault.
1898
1899 =head1 BUGS
1900
1901 Many settings can't be controlled through command line options yet.
1902
1903 =head1 SEE ALSO
1904
1905 privoxy(1)
1906
1907 =head1 AUTHOR
1908
1909 Fabian Keil <fk@fabiankeil.de>
1910
1911 =cut