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