#!/usr/bin/perl -w use strict; use Getopt::Long; use Cwd qw(getcwd); use File::Spec; use File::Temp; use File::Slurp; use File::Path qw(make_path); use Data::Dump qw/dd/; use FindBin; use LWP::UserAgent; use Digest::SHA qw(sha256_hex); use IO::CaptureOutput qw(capture_exec); use IO::Socket::INET; use JSON; use File::Copy; use Image::Resize; my %tests = ( tbbScreenshot => { type => 'mozmill' }, 'check.tpo' => { type => 'selenium' }, ); my %results; my %default_options = ( os => 'Linux', arch => 'x86_64', mozmill => 1, selenium => 1, starttor => 1, 'tor-control-port' => '9551', 'tor-socks-port' => '9550', ); my $options = get_options(@ARGV); sub exit_error { print STDERR "Error: ", $_[0], "\n"; chdir '/'; exit (exists $_[1] ? $_[1] : 1); } sub get_options { my @options = qw(mozmill! selenium! starttor! tor-control-port=i tor-socks-port=i reports-dir=s); my %res = %default_options; Getopt::Long::GetOptionsFromArray(\@_, \%res, @options) || exit 1; $res{files} = \@_; return \%res; } sub set_reports_dir { if ($options->{'reports-dir'}) { make_path($options->{'reports-dir'}); return; } my $r = $FindBin::Bin . '/reports'; mkdir $r; return $options->{'reports-dir'} = File::Temp::newdir( 'XXXXXX', DIR => $r, CLEANUP => 0)->dirname; } sub get_tbbfile { my ($tbbfile) = @_; if ($tbbfile =~ m/^https?:\/\//) { print "Downloading $tbbfile\n"; my (undef, undef, $file) = File::Spec->splitpath($tbbfile); my $output = "$options->{tbbinfos}{tmpdir}/$file"; my $ua = LWP::UserAgent->new; my $resp = $ua->get($tbbfile, ':content_file' => $output); exit_error "Error downloading $tbbfile:\n" . $resp->status_line unless $resp->is_success; return $output; } exit_error "File $tbbfile does not exist" unless -f $tbbfile; return $tbbfile; } sub tbb_filename_infos { my ($tbbfile) = @_; my (undef, undef, $file) = File::Spec->splitpath($tbbfile); my %res = (filename => $file); if ($file =~ m/^tor-browser-linux(..)-([^_]+)_(.+)\.tar\.xz$/) { @res{qw(type os version language)} = ('tbbfile', 'Linux', $2, $3); $res{arch} = $1 eq '64' ? 'x86_64' : 'x86'; } elsif ($file =~ m/^torbrowser-install-([^_]+)_(.+)\.exe$/) { @res{qw(type os arch version language)} = ('tbbfile', 'Windows', 'x86', $1, $2); } elsif ($file =~ m/^TorBrowserBundle-(.+)-osx32_(.+)\.zip$/) { @res{qw(type os arch version language)} = ('tbbfile', 'MacOSX', 'x86', $1, $2); } elsif ($file eq 'sha256sums.txt') { $res{type} = 'sha256sum'; } else { $res{type} = 'Unknown'; } return \%res; } sub extract_tbb { my ($tbbfile) = @_; exit_error "Can't open file $tbbfile" unless -f $tbbfile; $tbbfile = File::Spec->rel2abs($tbbfile); my $tmpdir = $options->{tbbinfos}{tmpdir}; chdir $tmpdir; system('tar', 'xf', $tbbfile); return "$tmpdir/tor-browser_$options->{tbbinfos}{language}"; } sub setup_tbb { $ENV{TOR_SKIP_LAUNCH} = 1; } sub monitor_bootstrap { my ($control_passwd) = @_; sleep 10; my $sock = new IO::Socket::INET( PeerAddr => 'localhost', PeerPort => $options->{'tor-control-port'}, Proto => 'tcp', ); exit_error "Error connecting to control port: $!\n" unless $sock; print $sock 'AUTHENTICATE "', $control_passwd, "\"\n"; my $r = <$sock>; exit_error "Authentication error: $r" unless $r =~ m/^250 OK/; my $i = 0; while (1) { print $sock "GETINFO status/bootstrap-phase\n"; $r = <$sock>; print $r; last if $r =~ m/^250-status\/bootstrap-phase.* TAG=done/; sleep 1; $i++; exit_error "Could not bootstrap after $i seconds" if $i > 300; } print "Bootstraping done\n"; return 3; } # TODO: In the future, we should start tor using tor-launcher sub start_tor { return unless $options->{starttor}; my $control_passwd = map { ('a'..'z', 'A'..'Z', 0..9)[rand 62] } 0..8; my $cwd = getcwd; $ENV{LD_LIBRARY_PATH} = "$cwd/Tor/"; $ENV{TOR_SOCKS_PORT} = $options->{'tor-socks-port'}; $ENV{TOR_CONTROL_PORT} = $options->{'tor-control-port'}; $ENV{TOR_CONTROL_HOST} = '127.0.0.1'; $ENV{TOR_CONTROL_COOKIE_AUTH_FILE} = "$cwd/Data/Tor/control_auth_cookie"; my ($hashed_password, undef, $success) = capture_exec("$cwd/Tor/tor", '--quiet', '--hash-password', $control_passwd); exit_error "Error running tor --hash-password" unless $success; chomp $hashed_password; my @torrc = read_file('Data/Tor/torrc-defaults'); foreach (@torrc) { s/^ControlPort .*/ControlPort $options->{'tor-control-port'}/; s/^SocksPort .*/SocksPort $options->{'tor-socks-port'}/; } write_file('Data/Tor/torrc-defaults', @torrc); my @cmd = ("$cwd/Tor/tor", '--defaults-torrc', "$cwd/Data/Tor/torrc-defaults", '-f', "$cwd/Data/Tor/torrc", 'DataDirectory', "$cwd/Data/Tor", 'GeoIPFile', "$cwd/Data/Tor/geoip", '__OwningControllerProcess', $$, 'HashedControlPassword', $hashed_password); $options->{tbbinfos}{torpid} = fork; if ($options->{tbbinfos}{torpid} == 0) { open(STDOUT, '>', '/dev/null'); open(STDERR, '>', '/dev/null'); exec @cmd; } return monitor_bootstrap($control_passwd); } sub stop_tor { return unless $options->{starttor}; kill 9, $options->{tbbinfos}{torpid}; } sub screenshot_thumbnail { my ($dir, $name) = @_; my $image = Image::Resize->new("$dir/$name"); write_file("$dir/t-$name", $image->resize(600, 600)->png); } sub mozmill_run { my ($test) = @_; return unless $options->{mozmill}; $test->{screenshots} = []; my %res = ( screenshots => [], ); my $screenshots_tmp = File::Temp::newdir; $ENV{'MOZMILL_SCREENSHOTS'} = $screenshots_tmp; my $results_file = "$options->{tbbinfos}{'results-dir'}/$test->{name}.json"; system('mozmill', '-b', "$options->{tbbdir}/Browser/firefox", '-p', "$options->{tbbdir}/Data/Browser/profile.default", '-t', "$FindBin::Bin/mozmill-tests/tbb-tests/$test->{name}.js", '--report', "file://$results_file"); my $i = 0; for my $screenshot_file (glob "$screenshots_tmp/*.png") { move($screenshot_file, "$options->{tbbinfos}{'results-dir'}/$test->{name}-$i.png"); screenshot_thumbnail($options->{tbbinfos}{'results-dir'}, "$test->{name}-$i.png"); push @{$test->{screenshots}}, "$test->{name}-$i.png"; $i++; } $test->{results} = decode_json(read_file($results_file)); } sub selenium_run { my ($test) = @_; return unless $options->{selenium}; system("$FindBin::Bin/selenium-tests/test_$test->{name}.py"); } sub run_tests { my ($tests) = @_; my %types = ( mozmill => \&mozmill_run, selenium => \&selenium_run, ); foreach my $test (keys %$tests) { $tests->{$test}{name} = $test; $types{$tests->{$test}{type}}->($tests->{$test}); } } sub matching_tbbfile { my $o = tbb_filename_infos($_[0]); return $o->{type} eq 'tbbfile' && $o->{os} eq $options->{os} && $o->{arch} eq $options->{arch}; } sub test_sha { my ($shafile) = @_; my $content; if ($shafile =~ m/^https?:\/\//) { my $ua = LWP::UserAgent->new; my $resp = $ua->get($shafile); exit_error "Error downloading $shafile:\n" . $resp->status_line unless $resp->is_success; $content = $resp->decoded_content; } else { $content = read_file($shafile); } my (undef, $dir) = File::Spec->splitpath($shafile); my @files = map { [ reverse split / /, $_ ] } split /\n/, $content; @files = grep { matching_tbbfile($_->[0]) } @files; foreach my $file (@files) { test_tbb("$dir/$file->[0]", $file->[1]); } } sub test_tbb { my ($tbbfile, $sha256sum) = @_; my $oldcwd = getcwd; $options->{tbbinfos} = tbb_filename_infos($tbbfile); return test_sha($tbbfile) if $options->{tbbinfos}{type} eq 'sha256sum'; $options->{tbbinfos}{tmpdir} = File::Temp::newdir; $tbbfile = get_tbbfile($tbbfile); if ($sha256sum && $sha256sum ne sha256_hex(read_file($tbbfile))) { exit_error "Wrong sha256sum for $tbbfile"; } $options->{tbbinfos}{tests} = { map { $_ => { %{$tests{$_}} } } keys %tests }; $options->{tbbinfos}{'results-dir'} = "$options->{'reports-dir'}/results-$options->{tbbinfos}{filename}"; mkdir $options->{tbbinfos}{'results-dir'}; $options->{tbbdir} = extract_tbb($tbbfile); chdir $options->{tbbdir} || exit_error "Can't enter directory $options->{tbbdir}"; $ENV{TBB_BIN} = "$options->{tbbdir}/Browser/firefox"; $ENV{TBB_PROFILE} = "$options->{tbbdir}/Data/Browser/profile.default"; start_tor; setup_tbb; print "tbbdir: $options->{tbbdir}\n"; run_tests($options->{tbbinfos}{tests}); chdir $oldcwd; stop_tor; $results{$options->{tbbinfos}{filename}} = $options->{tbbinfos}; } set_reports_dir; foreach my $tbbfile (@{$options->{files}}) { test_tbb($tbbfile); } print "Reports directory: $options->{'reports-dir'}\n";