277d4333a43d5133671860c400555a4a55e90c11
[privoxy.git] / tools / privoxy-regression-test.pl
1 #!/usr/bin/perl
2
3 ############################################################################
4 #
5 # Privoxy-Regression-Test
6 #
7 # A regression test "framework" for Privoxy. For documentation see:
8 # perldoc privoxy-regression-test.pl
9 #
10 # $Id: privoxy-regression-test.pl,v 1.115 2008/03/18 22:07:34 fk Exp $
11 #
12 # Wish list:
13 #
14 # - Update documentation
15 # - Validate HTTP times.
16 # - Understand default.action.master comment syntax
17 #   and verify that we actually block and unblock what
18 #   the comments claim we do.
19 # - Implement a HTTP_VERSION directive or allow to
20 #   specify whole request lines.
21 # - Support filter regression tests.
22 # - Add option to fork regression tests and run them in parallel,
23 #   possibly optional forever.
24 # - Document magic Expect Header values
25 # - Internal fuzz support?
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
43 use warnings;
44 use strict;
45 use Getopt::Long;
46
47 use constant {
48                PRT_VERSION => 'Privoxy-Regression-Test 0.2',
49  
50                CURL => 'curl',
51
52                # CLI option defaults
53                CLI_RETRIES  => 1,
54                CLI_LOOPS    => 1,
55                CLI_MAX_TIME => 5,
56                CLI_MIN_LEVEL => 0,
57                CLI_MAX_LEVEL => 25,
58
59                PRIVOXY_CGI_URL => 'http://p.p/',
60                FELLATIO_URL    => 'http://10.0.0.1:8080/',
61                LEADING_LOG_DATE => 1,
62                LEADING_LOG_TIME => 1,
63
64                DEBUG_LEVEL_FILE_LOADING    => 0,
65                DEBUG_LEVEL_PAGE_FETCHING   => 0,
66
67                VERBOSE_TEST_DESCRIPTION    => 1,
68
69                DEBUG_LEVEL_VERBOSE_FAILURE => 1,
70                # XXX: Only partly implemented and mostly useless.
71                DEBUG_LEVEL_VERBOSE_SUCCESS => 0,
72                DEBUG_LEVEL_STATUS          => 1,
73
74                # Internal use, don't modify
75                # Available debug bits:
76                LL_ERROR                   =>  1,
77                LL_VERBOSE_FAILURE         =>  2,
78                LL_PAGE_FETCHING           =>  4,
79                LL_FILE_LOADING            =>  8,
80                LL_VERBOSE_SUCCESS         => 16,
81                LL_STATUS                  => 32,
82                LL_SOFT_ERROR              => 64,
83
84                CLIENT_HEADER_TEST         =>  1,
85                SERVER_HEADER_TEST         =>  2,
86                DUMB_FETCH_TEST            =>  3,
87                METHOD_TEST                =>  4,
88                TRUSTED_CGI_REQUEST        =>  6,
89                BLOCK_TEST                 =>  7,
90 };
91
92 sub init_our_variables () {
93
94     our $leading_log_time = LEADING_LOG_TIME;
95     our $leading_log_date = LEADING_LOG_DATE;
96
97     our $privoxy_cgi_url  = PRIVOXY_CGI_URL;
98
99     our $verbose_test_description = VERBOSE_TEST_DESCRIPTION;
100
101     our $log_level = get_default_log_level();
102
103 }
104
105 sub get_default_log_level () {
106     
107     my $log_level = 0;
108
109     $log_level |= LL_FILE_LOADING    if DEBUG_LEVEL_FILE_LOADING;
110     $log_level |= LL_PAGE_FETCHING   if DEBUG_LEVEL_PAGE_FETCHING;
111     $log_level |= LL_VERBOSE_FAILURE if DEBUG_LEVEL_VERBOSE_FAILURE;
112     $log_level |= LL_VERBOSE_SUCCESS if DEBUG_LEVEL_VERBOSE_SUCCESS;
113     $log_level |= LL_STATUS          if DEBUG_LEVEL_STATUS;
114
115     # These are intended to be always on.
116     $log_level |= LL_SOFT_ERROR;
117     $log_level |= LL_ERROR;
118
119     return $log_level;
120 }
121
122 ############################################################################
123 #
124 # File loading functions
125 #
126 ############################################################################
127
128 sub parse_tag ($) {
129
130     my $tag = shift;
131
132     # Remove anchors
133     $tag =~ s@[\$\^]@@g;
134     # Unescape brackets and dots
135     $tag =~ s@\\(?=[{}().+])@@g;
136
137     # log_message("Parsed tag: " . $tag);
138
139     check_for_forbidden_characters($tag);
140
141     return $tag;
142 }
143
144 sub check_for_forbidden_characters ($) {
145
146     my $tag = shift; # XXX: also used to check values though.
147     my $allowed = '[-=\dA-Za-z{}:.\/();\s,+@"_%\?&]';
148
149     unless ($tag =~ m/^$allowed*$/) {
150         my $forbidden = $tag;
151         $forbidden =~ s@^$allowed*(.).*@$1@;
152
153         l(LL_ERROR, "'" . $tag . "' contains character '" . $forbidden. "' which is unacceptable.");
154     }
155 }
156
157 sub load_regressions_tests () {
158
159     our $privoxy_cgi_url;
160     our @privoxy_config;
161     my @actionfiles;
162     my $curl_url = '';
163     my $file_number = 0;
164
165     $curl_url .= $privoxy_cgi_url;
166     $curl_url .= 'show-status';
167
168     l(LL_STATUS, "Asking Privoxy for the number of action files available ...");
169
170     foreach (@{get_cgi_page_or_else($curl_url)}) {
171
172         chomp;
173         if (/<td>(.*?)<\/td><td class=\"buttons\"><a href=\"\/show-status\?file=actions&amp;index=(\d+)\">/) {
174
175             my $url = $privoxy_cgi_url . 'show-status?file=actions&index=' . $2;
176             $actionfiles[$file_number++] = $url;
177
178         } elsif (m@config\.html#.*\">([^<]*)</a>\s+(.*)<br>@) {
179
180             my $directive = $1 . " " . $2;
181             push (@privoxy_config, $directive);
182         }
183     }
184
185     l(LL_FILE_LOADING, "Recognized " . @actionfiles . " actions files");
186
187     load_action_files(\@actionfiles);
188 }
189
190 sub token_starts_new_test ($) {
191
192     my $token = shift;
193     my @new_test_directives =
194         ('set header', 'fetch test', 'trusted cgi request', 'request header', 'method test', 'block test');
195
196     foreach my $new_test_directive (@new_test_directives) {
197         return 1 if $new_test_directive eq $token;
198     }
199     return 0;
200
201 }
202
203 sub tokenize ($) {
204
205     my ($token, $value) = (undef, undef);
206
207     # Remove leading and trailing white space.
208     s@^\s*@@;
209     s@\s*$@@;
210
211     # Reverse HTML-encoding
212     # XXX: Seriously imcomplete. 
213     s@&quot;@"@g;
214
215     # Tokenize
216     if (/^\#\s*([^=:]*?)\s*[=]\s*(.*?)\s*$/) {
217
218         $token = $1;
219         $token =~ tr/[A-Z]/[a-z]/;
220         $value = $2;
221
222     } elsif (/^TAG\s*:(.*)$/) {
223
224         $token = 'tag';
225         $value = $1;
226
227     }
228
229     return ($token, $value);
230 }
231
232 sub enlist_new_test ($$$$$$) {
233
234     my ($regression_tests, $token, $value, $si, $ri, $number) = @_;
235
236     if ($token eq 'set header') {
237
238         l(LL_FILE_LOADING, "Header to set: " . $value);
239         ${$regression_tests}[$si][$ri]{'type'} = CLIENT_HEADER_TEST;
240         # Implicit default
241         $$regression_tests[$si][$ri]{'level'} = CLIENT_HEADER_TEST;
242
243     } elsif ($token eq 'request header') {
244
245         l(LL_FILE_LOADING, "Header to request: " . $value);
246         $$regression_tests[$si][$ri]{'type'} = SERVER_HEADER_TEST;
247         # Implicit default
248         $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
249         $$regression_tests[$si][$ri]{'level'} = SERVER_HEADER_TEST;
250
251     } elsif ($token eq 'trusted cgi request') {
252
253         l(LL_FILE_LOADING, "CGI URL to test in a dumb way: " . $value);
254         $$regression_tests[$si][$ri]{'type'} = TRUSTED_CGI_REQUEST;
255         # Implicit default
256         $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
257         $$regression_tests[$si][$ri]{'level'} = TRUSTED_CGI_REQUEST;
258
259     } elsif ($token eq 'fetch test') {
260
261         l(LL_FILE_LOADING, "URL to test in a dumb way: " . $value);
262         $$regression_tests[$si][$ri]{'type'} = DUMB_FETCH_TEST;
263         # Implicit default
264         $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
265         $$regression_tests[$si][$ri]{'level'} = DUMB_FETCH_TEST;
266
267     } elsif ($token eq 'method test') {
268
269         l(LL_FILE_LOADING, "Method to test: " . $value);
270         $$regression_tests[$si][$ri]{'type'} = METHOD_TEST;
271         # Implicit default
272         $$regression_tests[$si][$ri]{'expected-status-code'} = 200;
273         $$regression_tests[$si][$ri]{'level'} = METHOD_TEST;
274
275     } elsif ($token eq 'block test') {
276
277         l(LL_FILE_LOADING, "URL to block-test: " . $value);
278         $$regression_tests[$si][$ri]{'type'} = BLOCK_TEST;
279         # Implicit default
280         $$regression_tests[$si][$ri]{'expected-status-code'} = 403;
281         $$regression_tests[$si][$ri]{'level'} = BLOCK_TEST;
282
283     } else {
284
285         die "Incomplete '" . $token . "' support detected."; 
286
287     }
288
289     check_for_forbidden_characters($value);
290
291     $$regression_tests[$si][$ri]{'data'} = $value;
292
293     # For function that only get passed single tests
294     $$regression_tests[$si][$ri]{'section-id'} = $si;
295     $$regression_tests[$si][$ri]{'regression-test-id'} = $ri;
296     $$regression_tests[$si][$ri]{'number'} = $number - 1;
297     l(LL_FILE_LOADING,
298       "Regression test " . $number . " (section:" . $si . "):");
299 }
300
301 sub load_action_files ($) {
302
303     # initialized here
304     our %actions;
305     our @regression_tests;
306
307     my $actionfiles_ref = shift;
308     my @actionfiles = @{$actionfiles_ref};
309
310     my $si = 0;  # Section index
311     my $ri = -1; # Regression test index
312     my $count = 0;
313
314     my $ignored = 0;
315
316     l(LL_STATUS, "Loading regression tests from action file(s) delivered by Privoxy.");
317
318     for my $file_number (0 .. @actionfiles - 1) {
319
320         my $curl_url = ' "' . $actionfiles[$file_number] . '"';
321         my $actionfile = undef;
322
323         foreach (@{get_cgi_page_or_else($curl_url)}) {
324
325             my $no_checks = 0;
326             chomp;
327             
328             if (/<h2>Contents of Actions File (.*?)</) {
329                 $actionfile = $1;
330                 next;
331             }
332             next unless defined $actionfile;
333
334             last if (/<\/pre>/);
335
336             my ($token, $value) = tokenize($_);
337
338             next unless defined $token;
339
340             # Load regression tests
341
342             if (token_starts_new_test($token)) {
343
344                 # Beginning of new regression test.
345                 $ri++;
346                 $count++;
347                 enlist_new_test(\@regression_tests, $token, $value, $si, $ri, $count);
348             }
349
350             if ($token =~ /level\s+(\d+)/i) {
351
352                 my $level = $1;
353                 register_dependency($level, $value);
354             }
355             
356             if ($si == -1 || $ri == -1) {
357                 # No beginning of a test detected yet,
358                 # so we don't care about any other test
359                 # attributes.
360                 next;
361             }
362
363             if ($token eq 'expect header') {
364
365                 l(LL_FILE_LOADING, "Detected expectation: " . $value);
366                 $regression_tests[$si][$ri]{'expect-header'} = $value;
367
368             } elsif ($token eq 'tag') {
369                 
370                 next if ($ri == -1);
371
372                 my $tag = parse_tag($value);
373
374                 # We already checked in parse_tag() after filtering
375                 $no_checks = 1;
376
377                 l(LL_FILE_LOADING, "Detected TAG: " . $tag);
378
379                 # Save tag for all tests in this section
380                 do {
381                     $regression_tests[$si][$ri]{'tag'} = $tag; 
382                 } while ($ri-- > 0);
383
384                 $si++;
385                 $ri = -1;
386
387             } elsif ($token eq 'ignore' && $value =~ /Yes/i) {
388
389                 l(LL_FILE_LOADING, "Ignoring section: " . test_content_as_string($regression_tests[$si][$ri]));
390                 $regression_tests[$si][$ri]{'ignore'} = 1;
391                 $ignored++;
392
393             } elsif ($token eq 'expect status code') {
394
395                 l(LL_FILE_LOADING, "Expecting status code: " . $value);
396                 $regression_tests[$si][$ri]{'expected-status-code'} = $value;
397
398             } elsif ($token eq 'level') { # XXX: stupid name
399
400                 $value =~ s@(\d+).*@$1@;
401                 l(LL_FILE_LOADING, "Level: " . $value);
402                 $regression_tests[$si][$ri]{'level'} = $value;
403
404             } elsif ($token eq 'method') {
405
406                 l(LL_FILE_LOADING, "Method: " . $value);
407                 $regression_tests[$si][$ri]{'method'} = $value;
408
409             } else {
410
411                 # We don't use it, so we don't need
412                 $no_checks = 1;
413             }
414             # XXX: Neccessary?
415             check_for_forbidden_characters($value) unless $no_checks;
416             check_for_forbidden_characters($token);
417         }
418     }
419
420     l(LL_FILE_LOADING, "Done loading " . $count . " regression tests." 
421       . " Of which " . $ignored. " will be ignored)\n");
422 }
423
424 ############################################################################
425 #
426 # Regression test executing functions
427 #
428 ############################################################################
429
430 sub execute_regression_tests () {
431
432     our @regression_tests;
433     my $loops = get_cli_option('loops');
434     my $all_tests    = 0;
435     my $all_failures = 0;
436     my $all_successes = 0;
437
438     unless (@regression_tests) {
439
440         l(LL_STATUS, "No regression tests found.");
441         return;
442     }
443
444     l(LL_STATUS, "Executing regression tests ...");
445
446     while ($loops-- > 0) {
447
448         my $successes = 0;
449         my $tests = 0;
450         my $failures;
451         my $skipped = 0;
452
453         for my $s (0 .. @regression_tests - 1) {
454
455             my $r = 0;
456
457             while (defined $regression_tests[$s][$r]) {
458
459                 die "Section id mismatch" if ($s != $regression_tests[$s][$r]{'section-id'});
460                 die "Regression test id mismatch" if ($r != $regression_tests[$s][$r]{'regression-test-id'});
461
462                 my $number = $regression_tests[$s][$r]{'number'};
463
464                 if ($regression_tests[$s][$r]{'ignore'}
465                     or level_is_unacceptable($regression_tests[$s][$r]{'level'})
466                     or test_number_is_unacceptable($number)) {
467
468                     $skipped++;
469
470                 } else {
471
472                     my $result = execute_regression_test($regression_tests[$s][$r]);
473
474                     log_result($regression_tests[$s][$r], $result, $tests);
475
476                     $successes += $result;
477                     $tests++;
478                 }
479                 $r++;
480             }
481         }
482         $failures = $tests - $successes;
483
484         log_message("Executed " . $tests . " regression tests. " .
485             'Skipped ' . $skipped . '. ' . 
486             $successes . " successes, " . $failures . " failures.");
487
488         $all_tests    += $tests;
489         $all_failures += $failures;
490         $all_successes += $successes;
491
492     }
493
494     if (get_cli_option('loops') > 1) {
495         log_message("Total: Executed " . $all_tests . " regression tests. " .
496             $all_successes . " successes, " . $all_failures . " failures.");
497     }
498 }
499
500 sub level_is_unacceptable ($) {
501     my $level = shift;
502     return ((cli_option_is_set('level') and get_cli_option('level') != $level)
503             or ($level < get_cli_option('min-level'))
504             or ($level > get_cli_option('max-level'))
505             or dependency_unsatisfied($level)
506             );
507 }
508
509 sub test_number_is_unacceptable ($) {
510     my $test_number = shift;
511     return (cli_option_is_set('test-number')
512             and get_cli_option('test-number') != $test_number)
513 }
514
515 sub dependency_unsatisfied ($) {
516
517     my $level = shift;
518     our %dependencies;
519     our @privoxy_config;
520     my $dependency_problem = 0;
521
522     if (defined ($dependencies{$level}{'config line'})) {
523
524         my $dependency = $dependencies{$level}{'config line'};
525         $dependency_problem = 1;
526
527         foreach (@privoxy_config) {
528
529              $dependency_problem = 0 if (/$dependency/);
530         }
531     }
532
533     return $dependency_problem;
534 }
535
536 sub register_dependency ($$) {
537
538     my $level = shift;
539     my $dependency = shift;
540     our %dependencies;
541
542     if ($dependency =~ /config line\s+(.*)/) {
543
544        $dependencies{$level}{'config line'} = $1;
545     }
546 }
547
548 # XXX: somewhat misleading name
549 sub execute_regression_test ($) {
550
551     my $test_ref = shift;
552     my %test = %{$test_ref};
553     my $result = 0;
554
555     if ($test{'type'} == CLIENT_HEADER_TEST) {
556
557         $result = execute_client_header_regression_test($test_ref);
558
559     } elsif ($test{'type'} == SERVER_HEADER_TEST) {
560
561         $result = execute_server_header_regression_test($test_ref);
562
563     } elsif ($test{'type'} == DUMB_FETCH_TEST
564           or $test{'type'} == TRUSTED_CGI_REQUEST) {
565
566         $result = execute_dumb_fetch_test($test_ref);
567
568     } elsif ($test{'type'} == METHOD_TEST) {
569
570         $result = execute_method_test($test_ref);
571
572     } elsif ($test{'type'} == BLOCK_TEST) {
573
574         $result = execute_block_test($test_ref);
575
576     } else {
577
578         die "Unsupported test type detected: " . $test{'type'};
579
580     }
581
582     return $result;
583 }
584
585 sub execute_method_test ($) {
586
587     my $test_ref = shift;
588     my %test = %{$test_ref};
589     my $buffer_ref;
590     my $result = 0;
591     my $status_code;
592     my $method = $test{'data'};
593
594     my $curl_parameters = '';
595     my $expected_status_code = $test{'expected-status-code'};
596
597     $curl_parameters .= '--request ' . $method . ' ';
598     # Don't complain in case about the 'missing' body
599     $curl_parameters .= '--head ' if ($method =~ /^HEAD$/i);
600
601     $curl_parameters .= PRIVOXY_CGI_URL;
602
603     $buffer_ref = get_page_with_curl($curl_parameters);
604     $status_code = get_status_code($buffer_ref);
605
606     $result = check_status_code_result($status_code, $expected_status_code);
607
608     return $result;
609 }
610
611 sub execute_dumb_fetch_test ($) {
612
613     my $test_ref = shift;
614     my %test = %{$test_ref};
615     my $buffer_ref;
616     my $result = 0;
617     my $status_code;
618
619     my $curl_parameters = '';
620     my $expected_status_code = $test{'expected-status-code'};
621
622     if (defined $test{method}) {
623         $curl_parameters .= '--request ' . $test{method} . ' ';
624     }
625     if ($test{type} == TRUSTED_CGI_REQUEST) {
626         $curl_parameters .= '--referer ' . PRIVOXY_CGI_URL . ' ';
627     }
628
629     $curl_parameters .= $test{'data'};
630
631     $buffer_ref = get_page_with_curl($curl_parameters);
632     $status_code = get_status_code($buffer_ref);
633
634     $result = check_status_code_result($status_code, $expected_status_code);
635
636     return $result;
637 }
638
639 sub execute_block_test ($) {
640
641     my $test = shift;
642     my $url = $test->{'data'};
643     my $final_results = get_final_results($url);
644
645     return defined $final_results->{'+block'};
646 }
647
648 sub get_final_results ($) {
649
650     my $url = shift;
651     my $curl_parameters = '';
652     my %final_results = ();
653     my $final_results_reached = 0;
654
655     die "Unacceptable characterss in $url" if $url =~ m@[\\'"]@;
656     $url =~ s@\s@%20@g;
657
658     $curl_parameters .= "'" . PRIVOXY_CGI_URL . 'show-url-info?url=' . $url . "'";
659
660     foreach (@{get_cgi_page_or_else($curl_parameters)}) {
661
662         $final_results_reached = 1 if (m@<h2>Final results:</h2>@);
663
664         next unless ($final_results_reached);
665         last if (m@</td>@);
666
667         if (m@<br>([-+])<a.*>([^>]*)</a>( \{.*\})@) {
668             my $action = $1.$2;
669             my $value = $3;
670             $final_results{$action}{$value};
671         }
672     }
673
674     return \%final_results;
675 }
676
677 sub check_status_code_result ($$) {
678
679     my $status_code = shift;
680     my $expected_status_code = shift;
681     my $result = 0;
682
683     if ($expected_status_code == $status_code) {
684
685         $result = 1;
686         l(LL_VERBOSE_SUCCESS,
687           "Yay. We expected status code " . $expected_status_code . ", and received: " . $status_code . '.');
688
689     } elsif (cli_option_is_set('fuzzer-feeding') and $status_code == 123) {
690
691         l(LL_VERBOSE_FAILURE,
692           "Oh well. Status code lost while fuzzing. Can't check if it was " . $expected_status_code . '.');
693
694     } else {
695
696         l(LL_VERBOSE_FAILURE,
697           "Ooops. We expected status code " . $expected_status_code . ", but received: " . $status_code . '.');
698
699     }
700     
701     return $result;
702 }
703
704 sub execute_client_header_regression_test ($) {
705
706     my $test_ref = shift;
707     my $buffer_ref;
708     my $header;
709     my $result = 0;
710
711     $buffer_ref = get_show_request_with_curl($test_ref);
712
713     $header = get_header($buffer_ref, $test_ref);
714     $result = check_header_result($test_ref, $header);
715
716     return $result;
717 }
718
719 sub execute_server_header_regression_test ($) {
720
721     my $test_ref = shift;
722     my $buffer_ref;
723     my $header;
724     my $result = 0;
725
726     $buffer_ref = get_head_with_curl($test_ref);
727
728     $header = get_server_header($buffer_ref, $test_ref);
729     $result = check_header_result($test_ref, $header);
730
731     return $result;
732 }
733
734
735 sub interpret_result ($) {
736     my $success = shift;
737     return $success ? "Success" : "Failure";
738 }
739
740 sub check_header_result ($$) {
741
742     my $test_ref = shift;
743     my $header = shift;
744
745     my %test = %{$test_ref};
746     my $expect_header = $test{'expect-header'};
747     my $success = 0;
748
749     $header =~ s@   @ @g if defined($header);
750
751     if ($expect_header eq 'NO CHANGE') {
752
753         if (defined($header) and $header eq $test{'data'}) {
754
755             $success = 1;
756
757         } else {
758
759             $header //= "REMOVAL";
760             l(LL_VERBOSE_FAILURE,
761               "Ooops. Got: " . $header . " while expecting: " . $expect_header);
762         }
763
764     } elsif ($expect_header eq 'REMOVAL') {
765
766         if (defined($header) and $header eq $test{'data'}) {
767
768             l(LL_VERBOSE_FAILURE,
769               "Ooops. Expected removal but: " . $header . " is still there.");
770
771         } else {
772
773             # XXX: Use more reliable check here and make sure
774             # the header has a different name.
775             $success = 1;
776
777         }
778
779     } elsif ($expect_header eq 'SOME CHANGE') {
780
781         if (defined($header) and not $header eq $test{'data'}) {
782
783             $success = 1;
784
785         } else {
786
787             $header //= "REMOVAL";
788             l(LL_VERBOSE_FAILURE,
789               "Ooops. Got: " . $header . " while expecting: SOME CHANGE");
790         }
791
792
793     } else {
794
795         if (defined($header) and $header eq $expect_header) {
796
797             $success = 1;
798
799         } else {
800
801             $header //= "'No matching header'"; # XXX: No header detected to be precise
802             l(LL_VERBOSE_FAILURE,
803               "Ooops. Got: " . $header . " while expecting: " . $expect_header);
804         }
805     }
806     return $success;
807 }
808
809 sub get_header_name ($) {
810
811     my $header = shift;
812
813     $header =~ s@(.*?: ).*@$1@;
814
815     return $header;
816 }
817
818 sub get_header ($$) {
819
820     our $filtered_request = '';
821
822     my $buffer_ref = shift;
823     my $test_ref = shift;
824
825     my %test = %{$test_ref};
826     my @buffer = @{$buffer_ref};
827
828     my $expect_header = $test{'expect-header'};
829
830     my $line;
831     my $processed_request_reached = 0;
832     my $read_header = 0;
833     my $processed_request = '';
834     my $header;
835     my $header_to_get;
836
837     if ($expect_header eq 'REMOVAL'
838      or $expect_header eq 'NO CHANGE'
839      or  $expect_header eq 'SOME CHANGE') {
840
841         $expect_header = $test{'data'};
842
843     }
844
845     $header_to_get = get_header_name($expect_header);
846
847     foreach (@buffer) {
848
849         # Skip everything before the Processed request
850         if (/Processed Request/) {
851             $processed_request_reached = 1;
852             next;
853         }
854         next unless $processed_request_reached;
855
856         # End loop after the Processed request
857         last if (/<\/pre>/);
858
859         # Ditch tags and leading/trailing white space.
860         s@^\s*<.*?>@@g;
861         s@\s*$@@g;
862
863         $filtered_request .=  "\n" . $_;
864          
865         if (/^$header_to_get/) {
866             $read_header = 1;
867             $header = $_;
868             last;
869         }
870     }
871
872     return $header;
873 }
874
875 sub get_server_header ($$) {
876
877     my $buffer_ref = shift;
878     my $test_ref = shift;
879
880     my %test = %{$test_ref};
881     my @buffer = @{$buffer_ref};
882
883     my $expect_header = $test{'expect-header'};
884     my $header;
885     my $header_to_get;
886
887     if ($expect_header eq 'REMOVAL'
888      or $expect_header eq 'NO CHANGE'
889      or $expect_header eq 'SOME CHANGE') {
890
891         $expect_header = $test{'data'};
892
893     }
894
895     $header_to_get = get_header_name($expect_header);
896
897     foreach (@buffer) {
898
899         # XXX: should probably verify that the request
900         # was actually answered by Fellatio.
901         if (/^$header_to_get/) {
902             $header = $_;
903             $header =~ s@\s*$@@g;
904             last;
905         }
906     }
907
908     return $header;
909 }
910
911 sub get_status_code ($) {
912
913     my $buffer_ref = shift;
914     my @buffer = @{$buffer_ref}; 
915
916     foreach (@buffer) {
917
918         if (/^HTTP\/\d\.\d (\d{3})/) {
919
920             return $1;
921
922         } else {
923
924             return '123' if cli_option_is_set('fuzzer-feeding');
925             chomp;
926             l(LL_ERROR, 'Unexpected buffer line: "' . $_ . '"');
927         }
928     }
929 }
930
931 sub get_test_keys () {
932     return ('tag', 'data', 'expect-header', 'ignore');
933 }
934
935 # XXX: incomplete
936 sub test_content_as_string ($) {
937
938     my $test_ref = shift;
939     my %test = %{$test_ref};
940
941     my $s = "\n\t";
942
943     foreach my $key (get_test_keys()) {
944         $test{$key} = 'Not set' unless (defined $test{$key});
945     }
946
947     $s .= 'Tag: ' . $test{'tag'};
948     $s .= "\n\t";
949     $s .= 'Set header: ' . $test{'data'}; # XXX: adjust for other test types
950     $s .= "\n\t";
951     $s .= 'Expected header: ' . $test{'expect-header'};
952     $s .= "\n\t";
953     $s .= 'Ignore: ' . $test{'ignore'};
954
955     return $s;
956 }
957
958 ############################################################################
959 #
960 # HTTP fetch functions
961 #
962 ############################################################################
963
964 sub check_for_curl () {
965     my $curl = CURL;
966     l(LL_ERROR, "No curl found.") unless (`which $curl`);
967 }
968
969 sub get_cgi_page_or_else ($) {
970
971     my $cgi_url = shift;
972     my $content_ref = get_page_with_curl($cgi_url);
973     my $status_code = get_status_code($content_ref);
974
975     if (200 != $status_code) {
976
977         my $log_message = "Failed to fetch Privoxy CGI Page. " .
978                           "Received status code ". $status_code .
979                           " while only 200 is acceptable.";
980
981         if (cli_option_is_set('fuzzer-feeding')) {
982
983             $log_message .= " Ignored due to fuzzer feeding.";
984             l(LL_SOFT_ERROR, $log_message)
985
986         } else {
987
988             l(LL_ERROR, $log_message);
989
990         }
991     }
992     
993     return $content_ref;
994 }
995
996 sub get_show_request_with_curl ($) {
997
998     our $privoxy_cgi_url;
999     my $test_ref = shift;
1000     my %test = %{$test_ref};
1001
1002     my $curl_parameters = ' ';
1003
1004     # Enable the action to test
1005     $curl_parameters .= '-H \'X-Privoxy-Control: ' . $test{'tag'} . '\' ';
1006     # The header to filter
1007     $curl_parameters .= '-H \'' . $test{'data'} . '\' ';
1008
1009     $curl_parameters .= ' ';
1010     $curl_parameters .= $privoxy_cgi_url;
1011     $curl_parameters .= 'show-request';
1012
1013     return get_cgi_page_or_else($curl_parameters);
1014 }
1015
1016
1017 sub get_head_with_curl ($) {
1018
1019     our $fellatio_url = FELLATIO_URL;
1020     my $test_ref = shift;
1021     my %test = %{$test_ref};
1022
1023     my $curl_parameters = ' ';
1024
1025     # Enable the action to test
1026     $curl_parameters .= '-H \'X-Privoxy-Control: ' . $test{'tag'} . '\' ';
1027     # The header to filter
1028     $curl_parameters .= '-H \'X-Gimme-Head-With: ' . $test{'data'} . '\' ';
1029     $curl_parameters .= '--head ';
1030
1031     $curl_parameters .= ' ';
1032     $curl_parameters .= $fellatio_url;
1033
1034     return get_page_with_curl($curl_parameters);
1035 }
1036
1037
1038 sub get_page_with_curl ($) {
1039
1040     my $parameters = shift;
1041     my @buffer;
1042     my $curl_line = CURL;
1043     my $retries_left = get_cli_option('retries') + 1;
1044     my $failure_reason;
1045
1046     if (cli_option_is_set('privoxy-address')) {
1047         $curl_line .= ' --proxy ' . get_cli_option('privoxy-address');
1048     }
1049
1050     # We want to see the HTTP status code
1051     $curl_line .= " --include ";
1052     # Let Privoxy emit two log messages less.
1053     $curl_line .= ' -H \'Proxy-Connection:\' ' unless $parameters =~ /Proxy-Connection:/;
1054     $curl_line .= ' -H \'Connection: close\' ' unless $parameters =~ /Connection:/;
1055     # We don't care about fetch statistic.
1056     $curl_line .= " -s ";
1057     # We do care about the failure reason if any.
1058     $curl_line .= " -S ";
1059     # We want to advertise ourselves
1060     $curl_line .= " --user-agent '" . PRT_VERSION . "' ";
1061     # We aren't too patient
1062     $curl_line .= " --max-time '" . get_cli_option('max-time') . "' ";
1063
1064     $curl_line .= $parameters;
1065     # XXX: still necessary?
1066     $curl_line .= ' 2>&1';
1067
1068     l(LL_PAGE_FETCHING, "Executing: " . $curl_line);
1069
1070     do {
1071         @buffer = `$curl_line`;
1072
1073         if ($?) {
1074             $failure_reason = array_as_string(\@buffer);
1075             chomp $failure_reason;
1076             l(LL_SOFT_ERROR, "Fetch failure: '" . $failure_reason . $! ."'");
1077         }
1078     } while ($? && --$retries_left);
1079
1080     unless ($retries_left) {
1081         l(LL_ERROR,
1082           "Running curl failed " . get_cli_option('retries') .
1083           " times in a row. Last error: '" . $failure_reason . "'.");
1084     }
1085
1086     return \@buffer;
1087 }
1088
1089
1090 ############################################################################
1091 #
1092 # Log functions
1093 #
1094 ############################################################################
1095
1096 sub array_as_string ($) {
1097     my $array_ref = shift;
1098     my $string = '';
1099
1100     foreach (@{$array_ref}) {
1101         $string .= $_;
1102     }
1103
1104     return $string;
1105 }
1106
1107
1108 sub show_test ($) {
1109     my $test_ref = shift;
1110     log_message('Test is:' . test_content_as_string($test_ref));
1111 }
1112
1113 # Conditional log
1114 sub l ($$) {
1115     our $log_level;
1116     my $this_level = shift;
1117     my $message = shift;
1118
1119     return unless ($log_level & $this_level);
1120
1121     if (LL_ERROR & $this_level) {
1122         $message = 'Oh noes. ' . $message . ' Fatal error. Exiting.';
1123     }
1124
1125     log_message($message);
1126
1127     if (LL_ERROR & $this_level) {
1128         exit;
1129     }
1130 }
1131
1132 sub log_message ($) {
1133
1134     my $message = shift;
1135
1136     our $logfile;
1137     our $no_logging;
1138     our $leading_log_date;
1139     our $leading_log_time;
1140
1141     my $time_stamp = '';
1142     my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime time;
1143
1144     if ($leading_log_date || $leading_log_time) {
1145
1146         if ($leading_log_date) {
1147             $year += 1900;
1148             $mon  += 1;
1149             $time_stamp = sprintf("%i/%.2i/%.2i", $year, $mon, $mday);
1150         }
1151
1152         if ($leading_log_time) {
1153             $time_stamp .= ' ' if $leading_log_date;
1154             $time_stamp.= sprintf("%.2i:%.2i:%.2i", $hour, $min, $sec);
1155         }
1156         
1157         $message = $time_stamp . ": " . $message;
1158     }
1159
1160
1161     printf(STDERR "%s\n", $message);
1162
1163 }
1164
1165 sub log_result ($$) {
1166
1167     our $verbose_test_description;
1168     our $filtered_request;
1169
1170     my $test_ref = shift;
1171     my $result = shift;
1172     my $number = shift;
1173
1174     my %test = %{$test_ref};
1175     my $message = '';
1176
1177     $message .= interpret_result($result);
1178     $message .= " for test ";
1179     $message .= $number;
1180     $message .= '/';
1181     $message .= $test{'number'};
1182     $message .= '/';
1183     $message .= $test{'section-id'};
1184     $message .= '/';
1185     $message .= $test{'regression-test-id'};
1186     $message .= '.';
1187
1188     if ($verbose_test_description) {
1189
1190         if ($test{'type'} == CLIENT_HEADER_TEST) {
1191
1192             $message .= ' Header ';
1193             $message .= quote($test{'data'});
1194             $message .= ' and tag ';
1195             $message .= quote($test{'tag'});
1196
1197         } elsif ($test{'type'} == SERVER_HEADER_TEST) {
1198
1199             $message .= ' Request Header ';
1200             $message .= quote($test{'data'});
1201             $message .= ' and tag ';
1202             $message .= quote($test{'tag'});
1203
1204         } elsif ($test{'type'} == DUMB_FETCH_TEST) {
1205
1206             $message .= ' URL ';
1207             $message .= quote($test{'data'});
1208             $message .= ' and expected status code ';
1209             $message .= quote($test{'expected-status-code'});
1210
1211         } elsif ($test{'type'} == TRUSTED_CGI_REQUEST) {
1212
1213             $message .= ' CGI URL ';
1214             $message .= quote($test{'data'});
1215             $message .= ' and expected status code ';
1216             $message .= quote($test{'expected-status-code'});
1217
1218         } elsif ($test{'type'} == METHOD_TEST) {
1219
1220             $message .= ' HTTP method ';
1221             $message .= quote($test{'data'});
1222             $message .= ' and expected status code ';
1223             $message .= quote($test{'expected-status-code'});
1224
1225         } elsif ($test{'type'} == BLOCK_TEST) {
1226
1227             $message .= ' Supposedly-blocked URL: ';
1228             $message .= quote($test{'data'});
1229
1230         } else {
1231
1232             die "Incomplete support for test type " . $test{'type'} .  " detected.";
1233
1234         }
1235     }
1236
1237     log_message($message) unless ($result && cli_option_is_set('silent'));
1238 }
1239
1240 sub quote ($) {
1241     my $s = shift;
1242     return '\'' . $s . '\'';
1243 }
1244
1245 sub print_version () {
1246     printf PRT_VERSION . "\n" . 'Copyright (C) 2007-2008 Fabian Keil <fk@fabiankeil.de>' . "\n";
1247 }
1248
1249 sub help () {
1250
1251     our %cli_options;
1252
1253     print_version();
1254
1255     print << "    EOF"
1256
1257 Options and their default values if they have any:
1258     [--debug $cli_options{'debug'}]
1259     [--fuzzer-feeding]
1260     [--help]
1261     [--level]
1262     [--loops $cli_options{'loops'}]
1263     [--max-level $cli_options{'max-level'}]
1264     [--max-time $cli_options{'max-time'}]
1265     [--min-level $cli_options{'min-level'}]
1266     [--privoxy-address]
1267     [--retries $cli_options{'retries'}]
1268     [--silent]
1269     [--version]
1270 see "perldoc $0" for more information
1271     EOF
1272     ;
1273     exit(0);
1274 }
1275
1276 sub init_cli_options () {
1277
1278     our %cli_options;
1279     our $log_level;
1280
1281     $cli_options{'min-level'} = CLI_MIN_LEVEL;
1282     $cli_options{'max-level'} = CLI_MAX_LEVEL;
1283     $cli_options{'debug'}  = $log_level;
1284     $cli_options{'loops'}  = CLI_LOOPS;
1285     $cli_options{'max-time'}  = CLI_MAX_TIME;
1286     $cli_options{'retries'}  = CLI_RETRIES;
1287 }
1288
1289 sub parse_cli_options () {
1290
1291     our %cli_options;
1292     our $log_level;
1293
1294     init_cli_options();
1295
1296     GetOptions (
1297                 'debug=s' => \$cli_options{'debug'},
1298                 'help'     => sub { help },
1299                 'silent' => \$cli_options{'silent'},
1300                 'min-level=s' => \$cli_options{'min-level'},
1301                 'max-level=s' => \$cli_options{'max-level'},
1302                 'privoxy-address=s' => \$cli_options{'privoxy-address'},
1303                 'level=s' => \$cli_options{'level'},
1304                 'loops=s' => \$cli_options{'loops'},
1305                 'test-number=s' => \$cli_options{'test-number'},
1306                 'fuzzer-feeding' => \$cli_options{'fuzzer-feeding'},
1307                 'retries=s' => \$cli_options{'retries'},
1308                 'max-time=s' => \$cli_options{'max-time'},
1309                 'version'  => sub { print_version && exit(0) }
1310     );
1311     $log_level |= $cli_options{'debug'};
1312 }
1313
1314 sub cli_option_is_set ($) {
1315
1316     our %cli_options;
1317     my $cli_option = shift;
1318
1319     return defined $cli_options{$cli_option};
1320 }
1321
1322 sub get_cli_option ($) {
1323
1324     our %cli_options;
1325     my $cli_option = shift;
1326
1327     die "Unknown CLI option: $cli_option" unless defined $cli_options{$cli_option};
1328
1329     return $cli_options{$cli_option};
1330 }
1331
1332 sub main () {
1333
1334     init_our_variables();
1335     parse_cli_options();
1336     check_for_curl();
1337     load_regressions_tests();
1338     execute_regression_tests();
1339 }
1340
1341 main();
1342
1343 =head1 NAME
1344
1345 B<privoxy-regression-test> - A regression test "framework" for Privoxy.
1346
1347 =head1 SYNOPSIS
1348
1349 B<privoxy-regression-test> [B<--debug bitmask>] [B<--fuzzer-feeding>] [B<--help>]
1350 [B<--level level>] [B<--loops count>] [B<--max-level max-level>]
1351 [B<--max-time max-time>] [B<--min-level min-level>] B<--privoxy-address proxy-address>
1352 [B<--retries retries>] [B<--silent>] [B<--version>]
1353
1354 =head1 DESCRIPTION
1355
1356 Privoxy-Regression-Test is supposed to one day become
1357 a regression test suite for Privoxy. It's not quite there
1358 yet, however, and can currently only test client header
1359 actions and check the returned status code for requests
1360 to arbitrary URLs.
1361
1362 Client header actions are tested by requesting
1363 B<http://config.privoxy.org/show-request> and checking whether
1364 or not Privoxy modified the original request as expected.
1365
1366 The original request contains both the header the action-to-be-tested
1367 acts upon and an additional tagger-triggering header that enables
1368 the action to test.
1369
1370 =head1 CONFIGURATION FILE SYNTAX
1371
1372 Privoxy-Regression-Test's configuration is embedded in
1373 Privoxy action files and loaded through Privoxy's web interface.
1374
1375 It makes testing a Privoxy version running on a remote system easier
1376 and should prevent you from updating your tests without updating Privoxy's
1377 configuration accordingly.
1378
1379 A client-header-action test section looks like this:
1380
1381     # Set Header    = Referer: http://www.example.org.zwiebelsuppe.exit/
1382     # Expect Header = Referer: http://www.example.org/
1383     {+client-header-filter{hide-tor-exit-notation} -hide-referer}
1384     TAG:^client-header-filter\{hide-tor-exit-notation\}$
1385
1386 The example above causes Privoxy-Regression-Test to set
1387 the header B<Referer: http://www.example.org.zwiebelsuppe.exit/>
1388 and to expect it to be modified to
1389 B<Referer: http://www.example.org/>.
1390
1391 When testing this section, Privoxy-Regression-Test will set the header
1392 B<X-Privoxy-Control: client-header-filter{hide-tor-exit-notation}>
1393 causing the B<privoxy-control> tagger to create the tag
1394 B<client-header-filter{hide-tor-exit-notation}> which will finally
1395 cause Privoxy to enable the action section.
1396
1397 Note that the actions itself are only used by Privoxy,
1398 Privoxy-Regression-Test ignores them and will be happy
1399 as long as the expectations are satisfied.
1400
1401 A fetch test looks like this:
1402
1403     # Fetch Test = http://p.p/user-manual
1404     # Expect Status Code = 302
1405
1406 It tells Privoxy-Regression-Test to request B<http://p.p/user-manual>
1407 and to expect a response with the HTTP status code B<302>. Obviously that's
1408 not a very thorough test and mainly useful to get some code coverage
1409 for Valgrind or to verify that the templates are installed correctly.
1410
1411 If you want to test CGI pages that require a trusted
1412 referer, you can use:
1413
1414     # Trusted CGI Request =  http://p.p/edit-actions
1415
1416 It works like ordinary fetch tests, but sets the referer
1417 header to a trusted value.
1418
1419 If no explicit status code expectation is set, B<200> is used.
1420
1421 Additionally all tests have test levels to let the user
1422 control which ones to execute (see I<OPTIONS> below). 
1423 Test levels are either set with the B<Level> directive,
1424 or implicitly through the test type.
1425
1426 Fetch tests default to level 6, tests for trusted
1427 CGI requests to level 3 and client-header-action tests
1428 to level 1.
1429
1430 =head1 OPTIONS
1431
1432 B<--debug bitmask> Add the bitmask provided as integer
1433 to the debug settings.
1434
1435 B<--fuzzer-feeding> Ignore some errors that would otherwise
1436 cause Privoxy-Regression-Test to abort the test because
1437 they shouldn't happen in normal operation. This option is
1438 intended to be used if Privoxy-Regression-Test is only
1439 used to feed a fuzzer in which case there's a high chance
1440 that Privoxy gets an invalid request and returns an error
1441 message.
1442
1443 B<--help> Shows available command line options.
1444
1445 B<--level level> Only execute tests with the specified B<level>. 
1446
1447 B<--loop count> Loop through the regression tests B<count> times. 
1448 Useful to feed a fuzzer, or when doing stress tests with
1449 several Privoxy-Regression-Test instances running at the same
1450 time.
1451
1452 B<--max-level max-level> Only execute tests with a B<level>
1453 below or equal to the numerical B<max-level>.
1454
1455 B<--max-time max-time> Give Privoxy B<max-time> seconds
1456 to return data. Increasing the default may make sense when
1457 Privoxy is run through Valgrind, decreasing the default may
1458 make sense when Privoxy-Regression-Test is used to feed
1459 a fuzzer.
1460
1461 B<--min-level min-level> Only execute tests with a B<level>
1462 above or equal to the numerical B<min-level>.
1463
1464 B<--privoxy-address proxy-address> Privoxy's listening address.
1465 If it's not set, the value of the environment variable http_proxy
1466 will be used. B<proxy-address> has to be specified in http_proxy
1467 syntax.
1468
1469 B<--retries retries> Retry B<retries> times.
1470
1471 B<--silent> Don't log succesful test runs.
1472
1473 B<--version> Print version and exit.
1474
1475 The second dash is optional, options can be shortened,
1476 as long as there are no ambiguities.
1477
1478 =head1 PRIVOXY CONFIGURATION
1479
1480 Privoxy-Regression-Test is shipped with B<regression-tests.action>
1481 which aims to test all official client-header modifying actions
1482 and can be used to verify that the templates and the user manual
1483 files are installed correctly.
1484
1485 To use it, it has to be copied in Privoxy's configuration
1486 directory, and afterwards referenced in Privoxy's configuration
1487 file with the line:
1488
1489     actionsfile regression-tests.action
1490
1491 In general, its tests are supposed to work without changing
1492 any other action files, unless you already added lots of
1493 taggers yourself. If you are using taggers that cause problems,
1494 you might have to temporary disable them for Privoxy's CGI pages.
1495
1496 Some of the regression tests rely on Privoxy features that
1497 may be disabled in your configuration. Tests with a level below
1498 7 are supposed to work with all Privoxy configurations (provided
1499 you didn't build with FEATURE_GRACEFUL_TERMINATION).
1500
1501 Tests with level 9 require Privoxy to deliver the User Manual,
1502 tests with level 12 require the CGI editor to be enabled.
1503
1504 =head1 CAVEATS
1505
1506 Expect the configuration file syntax to change with future releases.
1507
1508 =head1 LIMITATIONS
1509
1510 As Privoxy's B<show-request> page only shows client headers,
1511 Privoxy-Regression-Test can't use it to test Privoxy actions
1512 that modify server headers.
1513
1514 As Privoxy-Regression-Test relies on Privoxy's tag feature to
1515 control the actions to test, it currently only works with
1516 Privoxy 3.0.7 or later.
1517
1518 At the moment Privoxy-Regression-Test fetches Privoxy's
1519 configuration page through I<curl>(1), therefore you have to
1520 have I<curl> installed, otherwise you won't be able to run
1521 Privoxy-Regression-Test in a meaningful way.
1522
1523 =head1 SEE ALSO
1524
1525 privoxy(1) curl(1)
1526
1527 =head1 AUTHOR
1528
1529 Fabian Keil <fk@fabiankeil.de>
1530
1531 =cut