From 6973b4772b6cc22f45ad42a8f9dad2d2a3cb1765 Mon Sep 17 00:00:00 2001 From: Ian Jackson Date: Thu, 25 Feb 2010 17:10:54 +0000 Subject: [PATCH] New secret ballot machinery --- .gitignore | 1 + ModerationCommon.pm | 53 ++++++++++++++ cgi/sballot | 1 + common.pl | 11 +++ sballot/cgi | 166 ++++++++++++++++++++++++++++++++++++++++++++ sballot/new-issue | 157 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 389 insertions(+) create mode 100644 ModerationCommon.pm create mode 120000 cgi/sballot create mode 100644 common.pl create mode 100755 sballot/cgi create mode 100755 sballot/new-issue diff --git a/.gitignore b/.gitignore index c3e4d59..83b6faf 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ /stump/etc/added-footer /stump/etc/added-footer.new /stump/etc/approval.key.txt +/sballot/issues /settings /moderators diff --git a/ModerationCommon.pm b/ModerationCommon.pm new file mode 100644 index 0000000..41f865f --- /dev/null +++ b/ModerationCommon.pm @@ -0,0 +1,53 @@ + +package ModerationCommon; + +BEGIN { + use Exporter (); + our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS); + + $VERSION = 1.00; + + @ISA = qw(Exporter); + @EXPORT = qw(hash $hashlen + readsettings %setting + sendmail_start sendmail_finish); + %EXPORT_TAGS = ( ); # eg: TAG => [ qw!name1 name2! ], + + @EXPORT_OK = qw(); +} + +our $hashlen=32; + +sub hash ($) { + my $r= `echo $_[0] | sha256sum`; $? and die $?; + $r =~ s/ *\-$//; + chomp $r; + return $r; +} + +sub sendmail_start () { + open ::P, "|sendmail -odb -oee -oi -t" or die $!; +} +sub sendmail_finish () { + $?=0; $!=0; close ::P or warn "$! $?"; +} + +our %setting; + +sub readsettingsfile ($) { + my ($file) = @_; + open SET, "<$file" or die "$file $!"; + while () { + next unless m/\S/; + next if m/^\#/; + m/^([A-Z_]+)\=(.*?)\s*$/ or die; + $setting{$1}= $2; + } +} + +sub readsettings () { + readsettingsfile("../../global-settings"); + readsettingsfile("../settings"); +} + +1; diff --git a/cgi/sballot b/cgi/sballot new file mode 120000 index 0000000..1acc459 --- /dev/null +++ b/cgi/sballot @@ -0,0 +1 @@ +../sballot/cgi \ No newline at end of file diff --git a/common.pl b/common.pl new file mode 100644 index 0000000..e149b91 --- /dev/null +++ b/common.pl @@ -0,0 +1,11 @@ +our $hashlen=32; + +sub hash ($) { + my $r= `echo $_[0] | sha256sum`; $? and die $?; + $r =~ s/ *\-$//; + chomp $r; + return $r; +} + + +1; diff --git a/sballot/cgi b/sballot/cgi new file mode 100755 index 0000000..d328b9b --- /dev/null +++ b/sballot/cgi @@ -0,0 +1,166 @@ +#!/usr/bin/perl -w + +use strict qw(refs vars); + +use CGI qw/:standard/; +use Cwd qw/realpath/; + +BEGIN { + my $self= $ENV{'SCRIPT_FILENAME'}; + $self= $0 unless defined $self; + $self= realpath $self; + my $sballotdir= $self; + $sballotdir =~ s,/[^/]*$,,; + + chdir $sballotdir or die "$sballotdir $!"; + unshift @INC, ".."; +}; + +use ModerationCommon; + +sub fail ($) { + my ($m)= @_; + print header(-status=>500), start_html('Secret ballot - error'), + h1("error"), strong($m), end_html(); + exit 0; +} + +my $issueid= param('issue'); +fail('bad issueid') if $issueid =~ m/[^-0-9a-z]/ or $issueid =~ m/^[^0-9a-z]/; + +open T, "issues/$issueid/title" or fail("unknown issue $!"); +my $title= ; chomp $title or die $!; +my $regexp= ; chomp $regexp or die $!; +close T or die $!; + +my $vote= param('vote'); +my $ident= param('ident'); +my $pw= param('password'); + +sub read_vfile ($) { + my ($vfile)= @_; + open M, $vfile or fail("unknown psuedonym $!"); + my $m= ; chomp $m or die $!; + close M or die $!; + + $m =~ m/^(\S+) (\S.*)$/ or die; + return ($1,$2); +} + +sub issue_and_title () { + return ( + dt('Issue ID'), dd(escapeHTML($issueid)), + dt('Title'), dd(escapeHTML($title)) + ); +} + +if (length $vote or length $ident or length $pw) { + fail('bad pseudonym') if !defined $ident or $ident =~ m/[^0-9a-z]/; + + fail('bad password') if !defined $pw or $pw =~ m/[^0-9a-z]/; + my $pwhash= hash($pw); + + fail('bad vote') if $vote =~ m/[^0-9a-z]/; + $vote =~ y/a-z/A-Z/; + + fail("invalid vote - consult administrator's instructions") + unless $vote =~ /^(?:$regexp)$/io; + + my $vfile= "issues/$issueid/v.$ident"; + my ($exp_pwhash, $oldvote) = read_vfile($vfile); + $exp_pwhash eq $pwhash or fail("wrong password"); + + open N, "> $vfile.new" or die $!; + print N "$pwhash $vote\n" or die $!; + close N or die $!; + + rename "$vfile.new", $vfile or die "$vfile $!"; + + print(header(), start_html('Secret ballot - vote recorded'), + h1('Vote recorded'), '
', + issue_and_title(), + dt('Old vote'), dd($oldvote), + dt('New vote'), dd($vote), '
', + end_html()) or die $!; + exit 0; +} + +if (param('results') or param('email_results')) { + my $txt= <) { + $vfile =~ m,/v\.([0-9a-f]+)([^/]*)$, or die; + next if $2 eq 'new'; + die "$vfile $2" if length $2; + $ident= $1; + my ($dummy_pwhash, $vote) = read_vfile($vfile); + $txt .= " $ident $vote\n"; + } + $txt .= < +Subject: Secret ballot results for $setting{ABBREV} + +One of the moderators for $setting{GROUP} +has requested that the results of the following ballot be sent out: + Issue ID: $issueid + Title: $title + +$txt + +Regards +moderation system robot +END + print(header(), start_html('Secret ballot - email sent'), + h1('Done'), + p('The email has been sent and should arrive shortly'), + end_html()) + or die $!; + exit 0; + } + print(header(), start_html('Secret ballot - results'), + h1('Results so far'), + '
',issue_and_title(),'
', + pre(escapeHTML($txt)), + end_html()) + or die $!; + exit 0; +} + +print(header(), start_html('Secret ballot - voting page'), + h1('Instructions'), + p('Wait for the email from the administrator confirming '. + 'that this is the actual live ballot before voting. '. + "The administrator's email will tell you what to put in". + " the vote box."), + h1('Voting form'), '
', + start_form(-method=>'POST'), + hidden('issue',$issueid), + issue_and_title(), + dt('Pseudonym'), dd(textfield(-name=>'ident', -size=>($hashlen+10))), + dt('Password'), dd(textfield(-name=>'password', -size=>($hashlen+10))), + dt('Vote'), dd(textfield(-name=>'vote', -size=>40)), + '
', + submit('Cast your vote'), + end_form(), + h1('Results'), + p('This allows you to view the results (so far)'), + start_form(-method=>'GET'), hidden('issue',$issueid), + p(submit(-name=>'results', + -value=>'Show results')), + p(submit(-name=>'email_results', + -value=>"Send results to moderators' list")), + end_form(), + end_html()) + or die $!; + +exit 0; diff --git a/sballot/new-issue b/sballot/new-issue new file mode 100755 index 0000000..d3720b6 --- /dev/null +++ b/sballot/new-issue @@ -0,0 +1,157 @@ +#!/usr/bin/perl -w +# +# usage: .../new-ballot '[YNA]' 'Remove Bad Bozo from the moderation panel' +# ^ regexp, put into /^(?:$regexp)$/io + +use strict qw(refs vars); + +BEGIN { + my $sballotdir= $0; $sballotdir =~ s,/[^/]*$,,; + chdir $sballotdir or die "$sballotdir $!"; + unshift @INC, '..'; +}; + +use ModerationCommon; +use POSIX; + +@ARGV==2 or die; +$ARGV[0] !~ m/^-/ or die; +my $regexp= shift @ARGV; +my $title= shift @ARGV; + +readsettings(); + +system "rm -rf issues/new"; +mkdir "issues" or $!==&EEXIST or die $!; +mkdir "issues/new",0770 or die $!; + +stat "issues/new" or die $!; +my $issueid= time.'-'.((stat _)[1]); +my $issuedir= "issues/$issueid"; + +rename "issues/new", $issuedir or die $!; + +open T, "> $issuedir/title" or die $!; +print T $title, "\n" or die $!; +print T $regexp, "\n" or die $!; +close T or die $!; + + +open S, "/dev/urandom" or die $!; + +sub randhex () { + my $nonce; + sysread(S, $nonce, $hashlen) == $hashlen or die $!; + return unpack "H*", $nonce; +} + +my @mods; + +open M, "../moderators" or die $!; +while () { + next unless m/\S/; + next if m/^\#/; + m/^([A-Z]+)\s+(\S+)\s*$/ or die; + my $m= { Name => $1, Email => $2 }; + $m->{Nonce}= randhex(); $m->{Ident}= hash($m->{Nonce}); + $m->{Password}= randhex(); $m->{HashedPw}= hash($m->{Password}); + push @mods, $m; +} +close M or die $!; + + +sendmail_start(); +print P < +Subject: Secret ballot initiated for $setting{ABBREV} + +The administrator of $setting{GROUP} +has initiated a new secret ballot on the question: + Issue ID: $issueid + Title: $title + +Each moderator will be sent a private email telling them their +pseudonym and voting details. + +There will also be an announcement from the administrator confirming +that this is the live ballot (as if there are problems with the +software, it may be necessary to initiate several) and explaining what +to enter into the "vote" box on the voting page. Please do not vote +until you've received that confirmation. + +The moderators who will be able to vote are the following people: +END +foreach my $m (@mods) { + my $opqe= $m->{Email}; + $opqe =~ s/\@/ (at) /; + printf P " %-10s %s\n", $m->{Name}, $opqe or die $!; +} + +print P <{Ident} cmp $b->{Ident} } @mods; +foreach my $m (@mods) { + my $vfile= "$issuedir/v.$m->{Ident}"; + open V, "> $vfile" or die "$vfile $!"; + print V "$m->{HashedPw} not voted\n" or die $!; + close V or die $!; + print P " ", $m->{Ident}, "\n" or die $!; +} + +print P <{Name} <$m->{Email}> +Subject: [$setting{ABBREV}] Secret ballot private info + +The administrator of $setting{GROUP} +has initiated a new secret ballot on the question: + Issue ID: $issueid + Title: $title + +Your login details for voting are: + Pseudonym: $m->{Ident} + Password: $m->{Password} + +These are confidential to you, valid only for this particular vote, +and cannot be regenerated if they are lost. So please keep this email +but do not reveal it to anyone. Reveal the password only to the +voting web page. + +There will also be an announcement from the administrator confirming +that this is the live ballot (as if there are problems with the +software, it may be necessary to initiate several) and explaining what +to enter into the "vote" box on the voting page. Please do not vote +until you've received that confirmation. + +You may then cast (and change) your vote here: + $setting{CGIBASEURL}/g.$setting{ABBREV}/sballot?issue=$issueid + +Thanks for your attention, +moderation system robot + +Addendum for the paranoid: +Your pseudonym was generated from a nonce, as follows: + echo $m->{Nonce} | sha256sum +Only you know this nonce - it is not stored on the moderation system +server. You should check that your vote is properly recorded and +complain if not: vote tallies will list which way each pseudonym +voted. +END + sendmail_finish(); +} -- 2.30.2