diff --git a/ballot.rb b/ballot.rb index 0acd58a..36623f7 100755 --- a/ballot.rb +++ b/ballot.rb @@ -1,917 +1,917 @@ =begin === This file is part of Ballot === Copyright 2011 Jeff Mitchell Ballot is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Ballot is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Ballot. If not, see . =end #!/usr/bin/env ruby require 'rubygems' require 'sinatra/base' require 'erb' require 'json' require 'digest/sha2' require 'net/ldap' require 'yubikey' require 'net/http' require 'net/https' require 'uri' require 'securerandom' require 'pony' require 'thread' $votedata = {} $votetimers = {} $remindersent = {} $scrambledata = {} $userprops = {} $gv_login_response = nil $votesmutex = Mutex.new # Unused right now -- it works but requires a personal google voice account def setupSMS $gv_login_response = nil gv_login_url = URI.parse('https://www.google.com/accounts/ClientLogin') gv_login_req = Net::HTTP::Post.new(gv_login_url.path) gv_login_req.form_data = {'accountType' => 'GOOGLE', 'Email' => $gv_email, 'Passwd' => $gv_passwd, 'service' => 'grandcentral', 'source' => 'Ballot'} gv_login_con = Net::HTTP.new(gv_login_url.host, gv_login_url.port) gv_login_con.use_ssl = true gv_login_con.start {|http| $gv_login_response = http.request(gv_login_req)} end # Reload votes. If it's no longer active, or expired, remove it from the active vote list # This will also fetch updates to the vote characteristics def checkVotes dir = Dir.new("#{$configdir}/votes") + puts "Checking votes in dir #{dir.to_s}" newvotedata = {} newvotetimers = {} dir.each { |filename| + puts "Checking #{filename}" if filename.end_with?(".vote") votename = filename.rpartition(".vote")[0] next if votename.nil? or votename.empty? hash = JSON.load(File.new("#{$configdir}/votes/#{filename}")) if not hash.has_key?("active") or not hash["active"] newvotedata.delete(votename) newvotetimers.delete(votename) next end newvotedata[votename] = hash newvotetimers[votename] = Time.now if not $remindersent.has_key?(votename) if hash["expiration"] >= Time.now.to_i + 86400 $remindersent[votename] = false else $remindersent[votename] = true end end end } $votesmutex.synchronize { $votedata = newvotedata $votetimers = newvotetimers } "success" end -# Start the thread to reload vote information and age entries out -Thread.new { - #FIXME: if I don't have a print statement below, this somehow no longer seems to - #run, after updates...wtf? - puts "Starting Thread" + +class Ballot < Sinatra::Base + puts "Starting Ballot" if $use_gv setupSMS end - loop do - checkVotes - sleep($vote_reload_time) - end -} - -class Ballot < Sinatra::Base + puts "Checking votes" + checkVotes + ############################################## #HELPERS# ############################################## helpers do $cookiedata = {} # Randomize a hash -- used in scramble votes to randomize the order of the options def randomizeHash(oldhash) startarray = oldhash.to_a endhash = {} rand = Random.new while not startarray.empty? val = startarray.delete_at(rand.rand(startarray.length)) endhash[val[0]] = val[1] end return endhash end # Unused for now, although it works def sendSMS(username, votename, scramblevalues) $votesmutex.synchronize { vote = $votedata[votename].clone } url = URI.parse('https://www.google.com/voice/sms/send/') req = Net::HTTP::Post.new(url.path, { 'Content-type' => 'application/x-www-form-urlencoded', 'Authorization' => 'GoogleLogin auth='+$gv_login_response.body.match("Auth\=(.*)")[0].gsub("Auth=", "") }) # We're sending the auth token back to google text = "For vote ID #{vote["id"]}:\n" scramblevalues.each { |choice, scramble| text += "#{scramble}: #{vote["choices"][choice]}\n" } req.form_data = {'id' => '', 'phoneNumber' => $userprops[username]["smsnumber"], 'text' => text, '_rnr_se' => $gv_rnr_se} con = Net::HTTP.new(url.host, url.port) con.use_ssl = true con.start {|http| response = http.request(req)} end # Essentially unused right now as sms is not yet fully implemented (without a personal Google Voice account) # and Yubikey works but requires some fields to go into Identity def populateUserProps(username, result) $userprops[username] = {} attributes = result.first.attribute_names if attributes.include?("yubikey") $userprops[username]["yubikey"] = result.first["yubikey"].first end # FIXME Yubikey gem doesn't support switching servers yet #if attributes.include?("yubiserver") # $userprops[username]["yubiserver"] = result.first["yubiserver"].first #end if attributes.include?("smsnumber") $userprops[username]["smsnumber"] = result.first["smsnumber"].first end end # Check the user's cookie. The cookie contains the username and a hash. # The hash is based on the username, the IP address (which prevents interception of the # session except by users behind the same NAT/firewall, and the random data # saved for that user. This function should be called on every page load. def checkCookie(givenname, givenhash) if $cookiedata.has_key?(givenname) and $cookiedata[givenname].has_key?("randomfloat") and $cookiedata[givenname].has_key?("randomprefix") and $cookiedata[givenname].has_key?("randomsuffix") and $cookiedata[givenname].has_key?("time") sha = Digest::SHA2.new(256) sha << givenname sha << $cookiedata[givenname]["randomfloat"].to_s if @env['REMOTE_ADDR'] == "127.0.0.1" sha << @env['HTTP_X_FORWARDED_FOR'] else sha << @env['REMOTE_ADDR'] end randomstring = "#{$cookiedata[givenname]["randomprefix"]}#{sha.to_s}#{$cookiedata[givenname]["randomsuffix"]}" if givenhash == randomstring if $cookiedata[givenname]["time"] + $session_timeout > Time.now refreshCookie(givenname) return "authenticated" else $cookiedata.delete(givenname) $userprops.delete(givenname) return "timedout" end else return "hashmismatched" end end return "fallthroughfail" end # Reset the user's cookie, using a new random value and updating the valid time. # This is called on every page load following a successful check of the cookie. def refreshCookie(username) $cookiedata[username] ||= {} prng = Random.new() $cookiedata[username]["randomfloat"] = prng.rand $cookiedata[username]["time"] = Time.now sha = Digest::SHA2.new(256) sha << username sha << $cookiedata[username]["randomfloat"].to_s if @env['REMOTE_ADDR'] == "127.0.0.1" sha << @env['HTTP_X_FORWARDED_FOR'] else sha << @env['REMOTE_ADDR'] end $cookiedata[username]["randomprefix"] = SecureRandom.hex(prng.rand( 1000 )) $cookiedata[username]["randomsuffix"] = SecureRandom.hex(prng.rand( 1000 )) randomstring = "#{$cookiedata[username]["randomprefix"]}#{sha.to_s}#{$cookiedata[username]["randomsuffix"]}" response.set_cookie("#{$cookie_domain}_user", { :value => username, :path => '/' } ) response.set_cookie("#{$cookie_domain}_hash", { :value => randomstring, :path => '/' } ) end # Check whether the user can use the given username/password to bind against the configured # LDAP server. If so, populate the user properties. def loginCheck(username, password) ldap = Net::LDAP.new ldap.host = $ldaphost ldap.port = $ldapport ldap.auth $binddn, $bindpw result = ldap.bind_as( :base => $authdn, :filter => "(#{$userattr}=#{username})", :password => password ) if not result or result.length == 0 return false end populateUserProps(username, result) return true end def getFilterSize(votename) filtersize = 0 $votesmutex.synchronize { if $votedata[votename].has_key?("groups") filtersize += $votedata[votename]["groups"].length end if $votedata[votename].has_key?("users") filtersize += $votedata[votename]["users"].length end } filtersize end def getFilter(votename) filter = "(|" $votesmutex.synchronize { if $votedata[votename].has_key?("groups") $votedata[votename]["groups"].each { |groupname| filter += "(#{$groupattr}=#{groupname})" } end if $votedata[votename].has_key?("users") $votedata[votename]["users"].each { |username| filter += "(#{$userattr}=#{username})" } end } filter += ")" filter end def getResults(votename) results = {} votedir = "#{$votebasedir}/#{votename}/values" dir = Dir.new(votedir) dir.each { |filename| next unless File.file?("#{votedir}/#{filename}") File.open("#{votedir}/#{filename}", "r") { |f| value = f.read.chomp.to_s if not results.has_key?(value) results[value] = { "votes" => 1, "hashes" => [filename] } else results[value]["votes"] = results[value]["votes"] + 1 results[value]["hashes"] = results[value]["hashes"] << filename end } } results end def sendMail(type, votename) votedata = nil $votesmutex.synchronize { votedata = $votedata[votename].clone } if not votedata.has_key?("mailattr") puts "Need mail attribute value for #{votename}" return end if getFilterSize(votename) == 0 return end filter = getFilter(votename) ldap = Net::LDAP.new ldap.host = $ldaphost ldap.port = $ldapport ldap.auth $binddn, $bindpw result = ldap.search( :base => $authdn, :filter => filter ) if result.length == 0 return end mailaddrs = [] result.each do |entry| entry.each do |attribute, values| + #puts attribute.to_s + #puts values.to_s if attribute.to_s == votedata["mailattr"] mailaddrs << values.first end end end mailtext = "" subject = "" if type.to_s == "invite" mailtext = $invitemail subject = "New vote available: \"#{votedata["description"]}\"" elsif type.to_s == "reminder" mailtext = $remindermail subject = "Reminder: please vote on \"#{votedata["description"]}\"" elsif type.to_s == "results" mailtext = $resultsmail subject = "Results of vote: \"#{votedata["description"]}\"" else puts "Cannot determine mail type, bailing" return end mailtext = mailtext.sub("__VOTENAME__", votedata["description"]) mailtext = mailtext.sub("__VOTEURL__", $siteurl) mailtext = mailtext.sub("__VOTEEXPIRATION__", Time.at(votedata["expiration"]).to_s) if type.to_s == "results" and votedata.has_key?("extra-result-mails") extramailaddrs = [] votedata["extra-result-mails"].each { |addr| extramailaddrs << addr mailaddrs << addr } mailtext = mailtext.sub("__VOTEEXTRAMAILS__", "This vote result is also being sent to #{extramailaddrs.to_s} for record-keeping purposes.\n") else mailtext = mailtext.sub("__VOTEEXTRAMAILS__", "\n") end if type.to_s == "results" and votedata["votetype"] == "ev-membership" resultstext = "" # check yes, no, abstain, and quorum results = getResults(votename) results.each_key { |key| if not votedata["choices"].has_key?(key) puts "Anomaly found -- some value doesn't match the vote choices!" return end } availablevotes = mailaddrs.size yes = (results.has_key?("yes") && results["yes"]["votes"]) || 0 no = (results.has_key?("no") && results["no"]["votes"]) || 0 abstain = (results.has_key?("abstain") && results["abstain"]["votes"]) || 0 totalvotes = yes + no + abstain quorum = (availablevotes*0.2).floor resultstext += "Number of active members: " + availablevotes.to_s + " (#{totalvotes} voted, #{(totalvotes * 100 / (availablevotes*1.0)).round(2)}%)\n" resultstext += "Number of \"Yes\" votes: " + yes.to_s + " (#{(yes * 100 / (totalvotes*1.0)).round(2)}%)\n" resultstext += "Number of \"No\" votes: " + no.to_s + " (#{(no * 100 / (totalvotes*1.0)).round(2)}%)\n" resultstext += "Number of \"Abstain\" votes: " + abstain.to_s + " (#{(abstain * 100 / (totalvotes*1.0)).round(2)}%)\n" resultstext += "Yes/No votes needed for quorum: " + quorum.to_s + " (got #{yes + no})\n" resultstext += "Vote valid: " + ((((yes + no) > quorum) && "Yes") || "No") + "\n" mailtext = mailtext.sub("__VOTERESULTS__", resultstext) hashestext = "Following are the counted votes. If you did not mark down your ticket when you\nvoted, you can recreate it by making a SHA256 sum, directly concatenating the\nfollowing values in a string, and using the command:\n\n\"echo -n | sha256sum\".\n\nIn order, the string must be: your username" if votedata["votehashsalt"].empty? hashestext += " and your secret key.\n\n" else hashestext += ", your secret key, and the vote\nsalt, which is:\n#{votedata["votehashsalt"]}\n\n" end results.keys.sort.each { |key| hashestext += "\n" i = 1 hashestext += "#{key} (#{results[key]["votes"].to_s}):\n" results[key]["hashes"].sort.each { |hash| hashestext += "\t#{i}: #{hash}\n" i = i + 1 } } mailtext = mailtext.sub("__VOTEHASHES__", hashestext) end puts "mail will be sent to #{mailaddrs}" puts mailtext mailaddrs.each do |addr| Pony.mail(:to => addr, :from => $mailsender, :subject => subject, :body => mailtext) end end def sendReminders() votedata = nil $votesmutex.synchronize { votedata = $votedata.clone } - $votedata.each_key do |votename| - if $votedata[votename]["expiration"] < Time.now.to_i + 86400 and not $remindersent[votename] + votedata.each_key do |votename| + if votedata[votename]["expiration"] < Time.now.to_i + 86400 and not $remindersent[votename] sendMail("reminder", votename) $remindersent[votename] = true end end return end # Check whether a user has valid credentials to access a given vote, see comments below # Note that there are no "return true" calls until the very end; all criteria must be satisfied def accessCheck(username, votename) failflag = false $votesmutex.synchronize { if not $votedata.has_key?(votename) or not $votetimers.has_key?(votename) or $votetimers[votename] + $vote_cache_time < Time.now or ($votedata[votename].has_key?("expiration") and $votedata[votename]["expiration"] < Time.now.to_i) # We didn't find the vote, or it needs to be reloaded, or it seems to not be valid anymore; check each of these explicitly # Does the vote exist? # Iterate through the vote directory and compare to prevent directory traversal attacks # instead of just looking at the filename and calling exists? votefound = false dir = Dir.new("#{$configdir}/votes") dir.each { |filename| if filename.end_with?(".vote") votefilename = filename.rpartition(".vote")[0] next if votefilename.nil? or votefilename.empty? or votefilename != votename votefound = true end } if not votefound $votedata.delete(votename) $votetimers.delete(votename) failflag = true end if not failflag # Reload the vote -- is it active, and is it valid (timewise)? hash = JSON.load(File.new("#{$configdir}/votes/#{votename}.vote")) if not hash.has_key?("active") or not hash["active"] or (hash.has_key?("expiration") and hash["expiration"] < Time.now.to_i) $votedata.delete(votename) $votetimers.delete(votename) failflag = true end end if not failflag # Store the refreshed vote information and continue on $votedata[votename] = hash $votetimers[votename] = Time.now end end # Is there *anyone* allowed to vote (check for existence of groups, users, or both)? - if not $votedata[votename].has_key?("groups") and not $votedata[votename].has_key?("users") + if not $votedata[votename] or (not $votedata[votename].has_key?("groups") and not $votedata[votename].has_key?("users")) failflag = true end } if failflag return false end # Is there *anyone* allowed to vote (check for existence of values for groups, users, or both)? if getFilterSize(votename) == 0 return false end # Create the LDAP filter to check membership, first by creating an ORed set of # OK groups and users filter = getFilter(votename) # Now add the current user with an AND -- so the current user must exist and it must be # be in at least one of the users or groups in the filter list above filter = "(&(#{$userattr}=#{username})" + filter + ")" ldap = Net::LDAP.new ldap.host = $ldaphost ldap.port = $ldapport ldap.auth $binddn, $bindpw result = ldap.search( :base => $authdn, :filter => filter ) if result.length == 0 return false end # Congrats, user -- you're clean populateUserProps(username, result) return true end # Search through the valid votes, and store any for which the user has access either # explicitly or via group membership def populateMyVotes(username, myvotes) votedata = nil $votesmutex.synchronize { votedata = $votedata.clone } votedata.each_key { |votename| if (votedata[votename].has_key?("administrators") and votedata[votename]["administrators"].include?(username)) or accessCheck(username, votename) myvotes[votename] = { "id" => votedata[votename]["id"], "description" => votedata[votename]["description"] } if votedata[votename].has_key?("expiration") myvotes[votename]["expiration"] = votedata[votename]["expiration"] end end } end end ############################################## #ROUTES# ############################################## # Show the front page get '/' do erb :index end # Show the about information get '/about/?' do erb :about end # Blank vote value, so redirect back to the front page get '/vote/?' do redirect '/' end # Log out -- remove the user cookies if the user is authed (if not, leave them be as it could be # someone else messing with the server trying to log out another user) get '/logout/?' do usercookie = request.cookies["#{$cookie_domain}_user"] hashcookie = request.cookies["#{$cookie_domain}_hash"] givenname = usercookie.to_s givenhash = hashcookie.to_s @votename = params[:votename] @status = "loggingout" authstatus = checkCookie(givenname, givenhash) if authstatus == "authenticated" $cookiedata.delete(givenname) $userprops.delete(givenname) @status = "loggedout" end erb :logout end # Log in page request. Display the yubikey field if configured to do so. # If the user is already authenticated, return them to the vote from whence they came # or show them a list of available votes get '/login/?' do usercookie = request.cookies["#{$cookie_domain}_user"] hashcookie = request.cookies["#{$cookie_domain}_hash"] givenname = usercookie.to_s givenhash = hashcookie.to_s @votename = params[:votename].to_s @status = "firstlogin" @use_yubikeys = $use_yubikeys authstatus = checkCookie(givenname, givenhash) if authstatus == "authenticated" $cookiedata[givenname]["time"] = Time.now if @votename == "allvotes" redirect "/votes" else redirect "/vote/#{@votename}" end return end erb :login end # Submission of login form. See inline comments below post '/login/?' do usercookie = request.cookies["#{$cookie_domain}_user"] hashcookie = request.cookies["#{$cookie_domain}_hash"] @votename = params[:votename].to_s @username = params[:username].to_s @password = params[:password].to_s @yubikeyotp = params[:yubikeyotp].to_s @use_yubikeys = $use_yubikeys # Did they fill in all values? if @username.nil? or @username.empty? or @password.nil? or @password.empty? @status = "notcomplete" # All values filled in, do the credentials pan out? elsif not loginCheck(@username, @password) @status = "notvalid" else # So far so good authedsofar = true # YubiKey code turned on? if $use_yubikeys authedsofar = false # If the user has a YubiKey in their Identity account, require it, but otherwise fall through if not $userprops[@username]["yubikey"] authedsofar = true else # Make sure the YubiKey they're using has the same public identifier as what's in Identity if @yubikeyotp.nil? or @yubikeyotp.empty? or $userprops[@username]["yubikey"] != @yubikeyotp[0,12] @status = "yubikeyinvalid" else begin otp = Yubikey::OTP::Verify.new(@yubikeyotp) if otp.valid? authedsofar = true elsif otp.replayed? @status = "yubikeyreplayed" end rescue Yubikey::OTP::InvalidOTPError @status = "yubikeyinvalid" end end end end if authedsofar refreshCookie(@username) if @votename == "allvotes" redirect "/votes" else redirect "/vote/#{@votename}" end return end end erb :login end # Display the available votes to the user get '/votes/?' do @votename = "allvotes" usercookie = request.cookies["#{$cookie_domain}_user"] hashcookie = request.cookies["#{$cookie_domain}_hash"] givenname = usercookie.to_s givenhash = hashcookie.to_s authstatus = checkCookie(givenname, givenhash) if authstatus != "authenticated" redirect "/login/?votename=#{@votename}" return end myvotes = {} populateMyVotes(givenname, myvotes) @votes = myvotes erb :votes end # Display a particular vote to the user. See inline comments. get '/vote/:votename' do @votename = params[:votename].to_s usercookie = request.cookies["#{$cookie_domain}_user"] hashcookie = request.cookies["#{$cookie_domain}_hash"] givenname = usercookie.to_s givenhash = hashcookie.to_s # If the user isn't authenticated, make them log in authstatus = checkCookie(givenname, givenhash) if authstatus != "authenticated" redirect "/login/?votename=#{@votename}" return end # Ensure they're allowed to see this vote @votedata = nil $votesmutex.synchronize { @votedata = $votedata[@votename].clone } if @votedata == nil status 404 return "Vote not active or does not exist" end @administrator = false if @votedata.has_key?("administrators") and @votedata["administrators"].include?(givenname) @administrator = true elsif not accessCheck(givenname, @votename) status 403 return "You are not authorized to participate in this vote." end @votecomplete = false if not @votedata.has_key?("expiration") or @votedata["expiration"] < Time.now.to_i @votecomplete = true end @scramblevalues = nil $scrambledata[@votename] ||= {} # If scrambling, scramble the values and store the decoded bits if @votedata.has_key?("scramble") and @votedata["scramble"] $scrambledata[@votename] ||= {} $scrambledata[@votename][givenname] = {} @votedata["choices"].each_key { |choice| $scrambledata[@votename][givenname][choice] = Random.new().rand(10000000) } @scramblevalues = randomizeHash($scrambledata[@votename][givenname]) if $use_gv and $gv_login_response and $userprops[givenname]["smsnumber"] @show_sms = true end end usersdir = "#{$usersbasedir}/#{@votename}" @status = "newvoter" usersha = Digest::SHA2.new(256) usersha << givenname usersha << @votedata["userhashsalt"] if File.exists?(usersdir + "/users/" + usersha.to_s) @status = "returnvoter" end erb :vote end # This is where the logic of recording the vote (or not) happens # Some various checks are skipped if we're sending an sms, because hitting the button # causes a POST but we can't expect a selection or key post '/vote/:votename' do @votename = params[:votename].to_s usercookie = request.cookies["#{$cookie_domain}_user"] hashcookie = request.cookies["#{$cookie_domain}_hash"] givenname = usercookie.to_s givenhash = hashcookie.to_s # Ensure they're authenticated authstatus = checkCookie(givenname, givenhash) if authstatus != "authenticated" redirect "/login/?votename=#{@votename}" return end # Ensure that they're allowed to see the vote - @votedata = $votedata[@votename] + $votesmutex.synchronize { + @votedata = $votedata[@votename] + } if @votedata == nil status 404 return "Vote not active or does not exist" end @administrator = false if @votedata.has_key?("administrators") and @votedata["administrators"].include?(givenname) @administrator = true elsif not accessCheck(givenname, @votename) status 403 return "You are not authorized to participate in this vote." end @votecomplete = false if not @votedata.has_key?("expiration") or @votedata["expiration"] < Time.now.to_i @votecomplete = true end # If the vote was scrambled, figure out the value that they actually selected $scrambledata[@votename] ||= {} if $scrambledata[@votename][givenname] $scrambledata[@votename][givenname].each { |key, value| if value == params["selection"].to_i @votevalue = key end } else @votevalue = params["selection"].to_s end skipvotecheck = false if params.has_key?("send_sms") or params.has_key?("sendinvitemail") or params.has_key?("sendremindermail") or params.has_key?("sendresultsmail") skipvotecheck = true end # Is the selection valid? if not @votedata["choices"].has_key?(@votevalue) and not skipvotecheck status 403 return "Not a valid vote selection" end # Is the key empty? @userkey = params["userkey"].to_s if (@userkey.nil? or @userkey.empty?) and not skipvotecheck status 403 return "Unique key cannot be empty" end # Save the vote value, since it's cleared if it's a scramble savedvotevalue = @votevalue # Generate new scramble values @scramblevalues = nil $scrambledata[@votename] ||= {} if @votedata.has_key?("scramble") and @votedata["scramble"] @votevalue = nil $scrambledata[@votename] ||= {} $scrambledata[@votename][givenname] = {} @votedata["choices"].each_key { |choice| $scrambledata[@votename][givenname][choice] = Random.new().rand(10000000) } @scramblevalues = randomizeHash($scrambledata[@votename][givenname]) if $use_gv and $gv_login_response and $userprops[givenname]["smsnumber"] @show_sms = true end end if skipvotecheck if params.has_key?("send_sms") and @show_sms sendSMS(givenname, @votename, @scramblevalues) @status = "smssent" elsif params.has_key?("sendinvitemail") sendMail("invite", @votename) @status = "invitemailsent" elsif params.has_key?("sendremindermail") sendMail("reminder", @votename) @status = "remindermailsent" elsif params.has_key?("sendresultsmail") sendMail("results", @votename) @status = "resultsmailsent" end else votedir = "#{$votebasedir}/#{@votename}" usersdir = "#{$usersbasedir}/#{@votename}" if not File.directory?(votedir) Dir.mkdir(votedir, 0700) end if not File.directory?(usersdir) Dir.mkdir(usersdir, 0700) end if not File.directory?(usersdir + "/users") Dir.mkdir(usersdir + "/users", 0700) end if not File.directory?(votedir + "/values") Dir.mkdir(votedir + "/values", 0700) end # Generate the vote and user hashes # The user hash consists of the username + the vote-specific user hash key # The vote hash consists of the username + the user's given key # If there is no user hash in existence, it's a new vote and create both files, # otherwise their given key must match the current filename in order to change it usersha = Digest::SHA2.new(256) usersha << givenname usersha << @votedata["userhashsalt"] votesha = Digest::SHA2.new(256) votesha << givenname votesha << @userkey votesha << @votedata["votehashsalt"] @uservotesha = votesha.to_s if not File.exists?(usersdir + "/users/" + usersha.to_s) userfile = File.new(usersdir + "/users/" + usersha.to_s, "w", 0600) userfile.close votefile = File.open(votedir + "/values/" + votesha.to_s, "w", 0600) votefile.write("#{savedvotevalue}\n") votefile.close @status = "voted" else if File.exists?(votedir + "/values/" + votesha.to_s) votefile = File.open(votedir + "/values/" + votesha.to_s, "w", 0600) votefile.write("#{savedvotevalue}\n") votefile.close @status = "voted" else @status = "failedchange" end end end erb :vote end get '/sendreminders' do sendReminders end get '/reloadvotes' do checkVotes end get '*' do redirect '/' end post '*' do redirect '/' end end