Article 3332 of comp.lang.perl: Xref: feenix.metronet.com comp.org.usenix:611 comp.lang.perl:3332 comp.org.ieee:422 Newsgroups: comp.org.usenix,comp.lang.perl,comp.org.ieee,comp.org.uniforum Path: feenix.metronet.com!news.utdallas.edu!tamsun.tamu.edu!cs.utexas.edu!sdd.hp.com!col.hp.com!csn!teal.csn.org!jsh From: jsh@teal.csn.org (Jeffrey Haemer) Subject: Electronic Balloting Message-ID: Sender: news@csn.org (The Daily Planet) Nntp-Posting-Host: teal.csn.org Organization: Colorado SuperNet, Inc. Date: Fri, 11 Jun 1993 02:13:30 GMT Lines: 637 Folks, A few weeks back, we posted a note asking for your advice and aid in prototyping software for electronic standards balloting. We got it. Thanks. Now we want some more. Here's a shar with a first cut at some balloting software. Unshar it, read the README, and the rest should be self-explanatory. We're looking for advice, bugs, bug-fixes, enhancements, and users. The shar includes our original post, explaining our goal in more detail. Jeffrey S. Haemer, USENIX Standards Liaiason Pat Wilson, SAGE Board Member ======== #! /bin/sh # This is a shell archive. Remove anything before this line, then unpack # it by saving it into a file and typing "sh file". To overwrite existing # files, type "sh file -c". You can also feed this as standard input via # unshar, or by typing "sh 'Environ.pl' <<'END_OF_FILE' X# $Id: Environ.pl,v 3.0 1993/06/04 19:53:34 ballot Exp ballot $ X Xpackage Environ; X# print the environment Xsub 'printenv { X foreach (sort keys %ENV) { X print "$_=$ENV{$_}\n"; X } X} X X# set environment variables X# override and add to the values in ENV X# with values specified in a resource file. X# The resource file is specified by RESOURCE Xsub 'setenv { X local(@path) = split(m%/%, $0); X local($cmd) = pop(@path); X %ENV = &'get_arr($cmd, %ENV) unless ($'setenv'guard++) X} X X# fill an array from a specified file X# Use the first argument as the name of an environment variable X# that points at the keyfile. X# If that fails, try a file by the name of arg1 X# Use the second argument as an array of default values X# X# See setenv() as an example. X# X# BUGS: X# I don't get why I can't combine the lines marked with "?". X# Xsub 'get_arr { X local($keyfile) = shift(@_); X local (%arr) = @_; X open(KEYS, $ENV{$keyfile} = $ENV{$keyfile} ? $ENV{$keyfile} : ("$keyfile.res") ) || return %arr; X while () { X chop; X ($name, $other) = split(' ', $_, 2); # ? X $arr{$name} = $other; # ? X } X close(KEYS) || die "can't close $keyfile: $!, aborting\n"; X return %arr; X} X X# Test routine. Invoke it in a driver with X# require "Environ.pl"; X# &Environ'test; Xsub test { X &'setenv; X %arr = &'get_arr("foo", %ENV); X die "get_arr failed to get ENV\n" unless $arr{HOME} = $ENV{HOME}; X $arr{HOME} = NULL; X X open(IN, "foo.res"); X print IN "HOME\tBOGUS\n"; X close(IN); X %arr = &'get_arr("foo", %ENV); X die "getarr failed to read foo.res\n" unless $arr{HOME} = "BOGUS"; X $arr{HOME} = NULL; X X $ENV{"bar"} = "foo.res"; X %arr = &'get_arr("bar", %ENV); X die "getarr failed to read ENV{bar}\n" unless $arr{HOME} = "BOGUS"; X $arr{HOME} = NULL; X X unlink("foo.res"); X print "Package tests okay.\n"; X} X X1; END_OF_FILE if test 1773 -ne `wc -c <'Environ.pl'`; then echo shar: \"'Environ.pl'\" unpacked with wrong size! fi chmod +x 'Environ.pl' # end of 'Environ.pl' fi if test -f 'Mail.pl' -a "${1}" != "-c" ; then echo shar: Will not clobber existing file \"'Mail.pl'\" else echo shar: Extracting \"'Mail.pl'\" \(2375 characters\) sed "s/^X//" >'Mail.pl' <<'END_OF_FILE' X# $Id: Mail.pl,v 3.0 1993/06/04 19:53:34 ballot Exp ballot $ X Xrequire "Environ.pl"; X Xpackage Mail; X X@MAILER = ("/usr/bin/mailx", "/bin/mail", "/usr/bin/mail", "/usr/ucb/Mail"); X X# get key lines X# Return each key value X# in the current item in an associative array. X# keylines are specified by regular expressions X# in a file named by the second argument, X# default expressions are supplied in the third. Xsub 'getkeys { X X local($_, $keyfile, %defaults) = @_; X X %keyline = &'get_arr($keyfile, %defaults) unless ($getkeys'guard++); X local(%info); X local($*) = 1; X for $k (keys %keyline) { X $info{$k} = $1 if (/$keyline{$k}/); X } X return %info; X} X X# get all the mail messages X# Toss the entire mail file into an array, one element per message. X# Return the array Xsub 'getmsgs { X open(INBOX, $_[0]) || die "can't open $_[0]: $!, aborting\n"; X local($/) = ''; X @msg = (); X $msg = ''; X while() { X if (/^From /) { X push(@msg, $msg) if $msg; X $msg = ''; X } X $msg .= $_; X } X push (@msg, $msg) if $msg; X return @msg; X} X X# mail a message X# just a call to the native mail program X# X# BUGS: I worry a bit about the diversity of mail programs Xsub 'mailx { X die "bad call to mailx\n" unless @_ > 2; # try assert X local($sub, $to, @msg) = @_; X ($ENV{MAILER}) = grep(-x, @MAILER) unless $ENV{MAILER}; X $cmd = "| $ENV{MAILER} -s '$sub' $to"; X open(ACKMAIL, $cmd) || die "can't open $cmd: $!, aborting\n"; X print ACKMAIL @msg; X close(ACKMAIL) || die "can't close $cmd: $!, aborting\n"; X} X X# Test routine. Invoke it in a driver with X# require "Mail.pl"; X# &Mail'test; X X# default values. %KEYLINES is probably the wrong set X@MAILDIR = ("/var/spool/mail", "/var/mail", "/usr/spool/mail"); X%KEYLINE = ( X 'Checksum', 'Checksum:\s*(\d+\s*\d+)', X 'From', '^From:\s*(.*)', X 'Author', 'Author:\s*(.*)', X 'ID', 'ID:\s*(\d+)', X 'Subject', '^Subject:\s*(.*)', X 'Vote', 'Vote:\s*(.+)', X ); X Xsub test { X setpwent; X ($name) = getpwuid($>); # I dunno, why not? X endpwent; X &'mailx("Zzazz", $name, "This is the first line\nThis is the second\n"); X sleep 10; X ($maildir) = grep(-e, @MAILDIR); X @msgs = &'getmsgs("$maildir/$name"); X $newmsg = pop(@msgs); X %info = &'getkeys($newmsg, "keyline", %KEYLINE); X X die "Bad subject line $info{Subject}\n" if "Zzazz" != $info{Subject}; X die "Bad recipient line $info{To}\n" if "Zzazz" != $info{To}; X print "Package tests okay.\n"; X} X X1; END_OF_FILE if test 2375 -ne `wc -c <'Mail.pl'`; then echo shar: \"'Mail.pl'\" unpacked with wrong size! fi # end of 'Mail.pl' fi if test -f 'README' -a "${1}" != "-c" ; then echo shar: Will not clobber existing file \"'README'\" else echo shar: Extracting \"'README'\" \(4209 characters\) sed "s/^X//" >'README' <<'END_OF_FILE' X# $Id: README,v 3.0 1993/06/04 19:53:34 ballot Exp ballot $ X XThis is a cut at balloting software. X X X INSTALLATION X XThe system receiving the votes should have a special id to process Xthe votes, and votes should be sent to that id (e.g. X). X XYou _must_ create an initial, two-column file of valid voters, X"ids.res". Each voter needs an id, and must know that id to vote. X(The format of the file is "ID Votername".) For IEEE standards Xballots, for example, you might want to make the list be a list of XIEEE members and their IEEE membership numbers. X XThe system has reasonable defaults, but it's fairly configurable; Xit lets you use external files to specify what kinds of votes can Xbe cast, where the mailboxes are, and all sorts of other things. XUnfortunately, you currently have to read the code to understand Xhow to configure it. X XCurrently, the scripts point at /usr/bin/perl. If your system has Xperl someplace else, you'll get non-informative error messages. X X X WHAT IT DOES X XVoters run "vote" to cast ballots. The recipient of the votes runs X"ballot" to count ballots. X X"vote" insures that the ballot has all the right fields, Xthen tags a checksum on the end. X X"ballot" runs through the incoming mailbox and sorts the votes into Xbins. In doing so, it validates the balloter and the checksum, Xand sends the sender an acknowledgment of the receipt of the vote. X X X WHAT IT DOESN'T DO: KNOWN PROBLEMS/PROJECTS X XThere should be a good configuration and installation script. X XCurrently, the scripts point at /usr/bin/perl. If your system has Xperl someplace else, you'll get non-informative error messages. XThis could probably be fixed with a good configuration and Xinstallation script. X XOther software that needs to be written are a utility for tallying Xvotes and a utility for processing invalid votes. We have contributed Xsoftware that parses valid votes once they're binned, but it's not yet Xready to post. X XThe "ids.res" file (and its processing) could be more sophisticated. XIn particular, if it contained an email address, we could have a Xdaemon periodically run through the file and send mail noodging Xthose who haven't yet voted. X XKnown problems awaiting future solution are indicated in comments Xthe code. Those problems should probably be listed here instead, Xor at least duplicated here. X XCurrently, the first vote from a voter that shows up in the mailbox Xis the one that counts. This has two shortcomings. First, if Xthe messages arrive out-of-order, this isn't the right choice. XSecond, we might want to allow people to change their minds, in Xwhich case we should probably use the _last_ message that arrives. XPerhaps this should be a configuration or command-line option. X XThere should be a man page that doesn't make you read the code to Xfigure out how to configure things. X X"ballot" doesn't make any effort to lock the input mailbox. That's Xbecause I don't really know how to do this. It also doesn't do Xvery fancy security, for the same reason. (Currently, "vote" appends Xa simple checksum to the message which is double-checked by "ballot", Xand then returned to the sender so that the sender can be certain that Xthe checksum received was the one sent.) X XBecause different groups may want different security levels or Xalgorithms, perhaps the security/checksumming should be done by a Xseparate executable that "vote" and "ballot" call as a subprocess. X X X PHILOSOPHICAL OBSERVATION X XIt is arguably ironic that this balloting software, initially Xdesigned to help ballot standards documents, is in a non-standardized Xlanguage. I suspect that the fastest way to whip this suite into Xshape is to ask that everyone who chooses to comment on this amusing Xirony accompany the comment with an improvement to the software. X(Figure, it's either that or we create a group to standardize perl ... :-) X X X FILES X XEnvPkg.pl A package to get and set environment variables and resources XMailPkg.pl A package that handles the mail messages XREADME This file Xballot Sorts mail file into bins of each kind of vote Xids.res A two-column list of folks yet to vote: ID Name Xvote script to generate a vote Xvoted.res Folks who have already voted, same format as "ids" END_OF_FILE if test 4209 -ne `wc -c <'README'`; then echo shar: \"'README'\" unpacked with wrong size! fi # end of 'README' fi if test -f 'ballot' -a "${1}" != "-c" ; then echo shar: Will not clobber existing file \"'ballot'\" else echo shar: Extracting \"'ballot'\" \(4819 characters\) sed "s/^X//" >'ballot' <<'END_OF_FILE' X#!/usr/bin/perl X# $Id: ballot,v 3.0 1993/06/04 19:53:34 ballot Exp ballot $ X Xrequire "Mail.pl"; X X@MAILDIRS = ("/var/spool/mail", "/var/mail", "/usr/spool/mail"); X%CHOICES = ( X YES, "yes", X NO, "no", X ABSTAIN, "abstain", X); X X%KEYLINE = ( X 'Checksum', 'Checksum:\s*(\d+\s*\d+)', X 'From', '^From:\s*(.*)', X 'Author', 'Author:\s*(.*)', X 'ID', 'ID:\s*(\d+)', X 'Subject', '^Subject:\s*(.*)', X 'Vote', 'Vote:\s*(.+)', X ); X X# acknowledge a message X# parse out the sender, X# return an ack with enough information to let the recipient know X# what we actually got. Xsub ack { X local($sub) = "Vote received"; X local($to) = $Keyline{From}; X local(@ack) = ( X "Received vote: $Keyline{Vote}\n", X "Voter: $Keyline{Author}\n", X "ID: $Keyline{ID}\n", X "Checksum: $Keyline{Checksum}\n", X "Msg subject: $Keyline{Subject}\n" X ); X X # optional "To" line formats X if ($to =~ /<(\S*)>/) { X $to = $1; X } elsif ($to =~ /(\S*)\s*\(.*\)/) { X $to = $1; X } else { X return 0; X } X &mailx($sub, $to, @ack); X 1; X} X X# 'bin' an individual message X# Make sure the vote's legit and acknowledge it, X# then toss the message into the bin X# indicated in the message's "Vote:" field. Xsub bin { X local($msg) = @_[0]; X %Keyline = &'getkeys($msg, "keyline", %KEYLINE); X local($vote) = $Keyline{Vote}; X foreach (keys %Choices) { X $vote = $_, last if $vote =~ /$Choices{$_}/i X } X $vote = BAD unless (&valid($msg) && $vote && &ack()); X $tally{$vote}++; X print $vote $msg; X} X X# check out the checksum X# Right now, expects messages to end with the two lines X# Checksum: nnnn nnnn X# X# where the numbers come from running cksum X# on the rest of the message body X# X# BUGS: Okay, okay, this isn't any good. X# Still, little point wasting time here X# until I know what the right thing to do is. Xsub cksum { X local($msg) = @_[0]; X return 1; X # $*=1; X @text = split(/\n\n/, $msg); # split out the header X shift(@text); # and dump it X @text = split(/^Checksum: /, join(' ', @text)); # now the get the checksum X $checksum = pop(@text); X $checksum = unpack("%32C*", join(' ', @text)); X} X X# clean up X# print out the tally; X# rewrite the "ids" (yet-to-vote) and "voted" files; X# finally close open files X# removing any that got created but never used Xsub clean_up { X &tally(%tally); X open(IDS, "> $ENV{'ids'}") || die "can't open $ENV{ids} for write: $!, aborting\n"; X open(VOTED, ">> $ENV{'voted'}") || die "can't open $ENV{voted} for append: $!, aborting\n"; X foreach (keys %Ids) { X print { $Voted{$_} ? VOTED : IDS } "$_\t$Ids{$_}\n"; X } X close(IDS) || die "can't close $ENV{ids}: $!, aborting\n"; X close(VOTED) || die "can't close $ENV{voted}: $!, aborting\n"; X close(BAD) || die "can't close BAD: $!, aborting"; X foreach (BAD, $ENV{"ids"}, $ENV{"voted"}) { X unlink $_ if -z $_; X } X X unlink("BAD") if -z "BAD"; X foreach (keys %Choices) { X close($_) || die "can't close $_: $!, aborting"; X unlink $_ if -z $_; X } X} X X# all the initialization X# open up the input mailbox and the output mailboxes. X# note that votes are cumulative X# X# BUGS: needs to lock the input mailbox, X# then move it out of the way so ballots aren't counted twice Xsub initialize { X &setenv; X die "No valid voters\n" unless %Ids = &get_arr("ids"); X local(%voted) = &get_arr("voted"); X foreach (keys %voted) { X $Voted{$_}++; X } X X ($Maildir) = grep(-e, @MAILDIRS); X ($Name) = getpwuid($>); # I dunno, why not? X $ENV{BALLOT_BOX} = $ENV{BALLOT_BOX} || "$Maildir/$Name"; X $ENV{"voted"} = $ENV{"voted"} || "voted.res"; X die "No votes\n" if ( -z $ENV{BALLOT_BOX} || ! -e $ENV{BALLOT_BOX} ); X %Choices = &get_arr("choices", %CHOICES); X foreach (keys %Choices) { X open($_, ">> $_") || die "can't open $_ for append: $!, aborting\n"; X } X open(BAD, ">> BAD") || die "can't open BAD for append: $!, aborting\n"; X} X X# tally the votes X# just print out the tallies from this batch of mail X# X# BUGS: This is just a debugging tool. X# Tallying should be done by a separate process X# and go directly to the tally bins. Xsub tally { X local(%tally) = @_; X foreach (sort keys %tally) { X print "$_=$tally{$_}\n"; X } X} X X# validate the message X# Read the file that matches IDs to voter names, X# then check that the message "ID:" field has the right "Name:" field. X# and that the checksum matches the message. X# Don't allow ballot-box stuffing -- only vote once. X# X# BUGS: May need other checks. X# X# Assumes access to a global %Keyline array, X# which is probably the wrong thing to do. X# This should all be modularized and stuff. Xsub valid { X local($msg) = @_[0]; X ++$Voted{$Keyline{ID}} if X ($Keyline{Author} eq $Ids{$Keyline{ID}}) X && &cksum($msg) X && !$Voted{$Keyline{ID}}; X} X X# read mail messages from mailbox X# and toss them one-by-one into an appropriate output mailbox X X&initialize; X@msg = &getmsgs($ENV{BALLOT_BOX}); Xwhile($msg = shift(@msg)) { X &bin($msg); X} X&clean_up; END_OF_FILE if test 4819 -ne `wc -c <'ballot'`; then echo shar: \"'ballot'\" unpacked with wrong size! fi chmod +x 'ballot' # end of 'ballot' fi if test -f 'balloting.mm' -a "${1}" != "-c" ; then echo shar: Will not clobber existing file \"'balloting.mm'\" else echo shar: Extracting \"'balloting.mm'\" \(1852 characters\) sed "s/^X//" >'balloting.mm' <<'END_OF_FILE' X.\" $Id: balloting.mm,v 1.1 1993/06/04 20:10:15 ballot Exp ballot $ X.\" Uses the mm macro ".PH", but that's all X.PH "''''" X.ce XElectronic Balloting Software X XWhat if you could FTP POSIX draft standards and ballot on them from Xthe comfort of your own keyboard? No more messy stamps or ink-stained Xfingers! No need to wait on the Postal "Service" for the latest Xdrafts! X XWe see a future in which draft standards will be available for Xanonymous FTP, and balloting can be done by email. For a variety Xof reasons -- some sensible, some merely historical -- achieving Xeither of these advances requires overcoming both political and Xtechnical barriers. We'd like to remove the technical barriers. XAndrew Hume has already demonstrated a prototype solution for Xelectronic draft distribution. We'd like to see a prototype for Xelectronic balloting, and we'd like your help to make this happen. X XIt's envisioned that any electronic balloting procedure will need X(a) authentication at least as good as Snail Mail provides and (b) Xvote-counting software to make it as painless as possible. X XQuick-and-dirty authentication can be as simple as (1) mail the Xballot-request software, which then (2) generates an encryption Xkey and (3) sends it, via postal mail (wouldn't want them to feel X_too_ left out!) to the requester, who then (4) uses the out-of-band Xkey to encrypt the ballot, which is then (5) decrypted by the Xballoting software and (6) counted appropriately -- or at least Xthat's the plan. X XWhat's wrong with the plan? How can it be made better? How Xinteresting can the vote-counting software get? Should it be Xwritten in perl? X XPlease contribute your thoughts, code, etc., and help POSIX balloting Xstep into the 1990s. X X X.nf X- Jeffrey S. Haemer, USENIX Standards Liaison X- Pat Wilson, SAGE Board Member X.fi END_OF_FILE if test 1852 -ne `wc -c <'balloting.mm'`; then echo shar: \"'balloting.mm'\" unpacked with wrong size! fi # end of 'balloting.mm' fi if test -f 'vote' -a "${1}" != "-c" ; then echo shar: Will not clobber existing file \"'vote'\" else echo shar: Extracting \"'vote'\" \(836 characters\) sed "s/^X//" >'vote' <<'END_OF_FILE' X#!/usr/bin/perl X# $Id: vote,v 3.0 1993/06/04 19:53:34 ballot Exp ballot $ X X# send in a ballot X# Tags a checksum on the end. X# X# BUGS: X# The program should probably save the checksum, X# or even the completed ballot, somewhere X# so the user can verify that the acknowledgement X# matches what was sent. X# Right now, I use mail's "set record=" feature for this. X Xrequire "Mail.pl"; X X@KEYWORDS = (Author, ID, Vote); X Xdie "Usage $0 ballot address\n" if (@ARGV != 2); Xopen(BALLOT, $ARGV[0]); Xwhile () { X push (@msg, $_); X foreach $k (@KEYWORDS) { X $keywords{$k}++ if ($_ =~ /^$k/); X } X} X Xforeach $k (@KEYWORDS) { X unless ($keywords{$k}) { X print "$k? "; X $_ = <>; X redo if $_ =~ /^\s+$/; X push(@msg, "$k: $_"); X next; X } X} Xpush (@msg, "Checksum: " . unpack('%32C*', join('', @msg)) . "\n"); X X&mailx("Vote", $ARGV[1], @msg); END_OF_FILE if test 836 -ne `wc -c <'vote'`; then echo shar: \"'vote'\" unpacked with wrong size! fi chmod +x 'vote' # end of 'vote' fi echo shar: End of archive 1 \(of 1\). cp /dev/null ark1isdone MISSING="" for I in 1 ; do if test ! -f ark${I}isdone ; then MISSING="${MISSING} ${I}" fi done if test "${MISSING}" = "" ; then echo You have the archive. rm -f ark[1-9]isdone else echo You still need to unpack the following archives: echo " " ${MISSING} fi ## End of shell archive. exit 0