add documentation to functions and:

- use single quotes where possible
- explicitly initialize mysql bind variables
- change the sync_command() to one file per call
- sanitize inputs for export_result endpoint
- add JSON decode error handling
This commit is contained in:
Christian Staudte 2020-11-04 20:12:50 +01:00
parent 8743fd12a5
commit 5f7db5f8ea
Signed by: christian.staudte
GPG Key ID: 88ED5070FE0D5F23
3 changed files with 152 additions and 88 deletions

View File

@ -23,6 +23,5 @@ rawpath = /path/to/json/dir
; path in which signed files will be stored
path = /path/to/dir
; synccmd can be empty, if no synchronization command is needed. Placeholders:
; %s - signed file
; %u - unsigned file
;synccmd = rclone %s Remote:dir
; %f - filename
;synccmd = rclone %f Remote:dir

View File

@ -14,74 +14,82 @@ class AbstimmIDd {
$this->cfg['database']['password'] = '';
}
function get_voting_ids($body) {
$event_id = $this->get_event_id($body->event_token);
$vote_round = $body->round;
$result = [];
if (!$event_id) {
return ["error" => "event not found"];
/**
* Utility function to get the event ID for an event token
* @param string $token The UUID of the event
* @return int The ID if the event was found, 0 otherwise
*/
function get_event_id(string $token) : int {
$query = 'SELECT id FROM events WHERE token=? LIMIT 1';
$stmt = $this->mysqli->prepare($query);
$stmt->bind_param('s', $token);
$stmt->execute();
$event_id = 0;
$stmt->bind_result($event_id);
$stmt->fetch();
$stmt->close();
return (int)$event_id;
}
/**
* API endpoint to register an event token
* @param mixed $body The JSON object as specified in the API documentation
* @return mixed The JSON response object
*/
function register_event($body) {
// check if the event already exists
if ($this->get_event_id($body->event_token) !== 0) {
return ['success' => false];
}
// otherwise, create the event
$query = 'INSERT INTO events (token) VALUES (?)';
$stmt = $this->mysqli->prepare($query);
$stmt->bind_param('s', $body->event_token);
$stmt->execute();
$stmt->close();
return ['success' => true];
}
/**
* API endpoint to get voting IDs (will be generated on demand if needed)
* @param mixed $body The JSON object as specified in the API documentation
* @return mixed The JSON response object
*/
function get_voting_ids($body) {
if (($event_id = $this->get_event_id($body->event_token)) === 0) {
return ['error' => 'event not found'];
}
$vote_round = (int)$body->round;
$result = [];
foreach($body->user_names as $name) {
// skip empty names (e.g. if batch processing text files with empty lines)
if (strlen($name) > 0) {
$hash = $this->get_hash($event_id, $vote_round, $name);
$result[] = ["round" => $vote_round, "user_name" => $name, "hash" => $hash];
$result[] = ['round' => $vote_round, 'user_name' => $name, 'hash' => $hash];
}
}
return $result;
}
function get_event_id($token) {
$query = "SELECT id FROM events WHERE token=? LIMIT 1";
$stmt = $this->mysqli->prepare($query);
$stmt->bind_param('s', $token);
$stmt->bind_result($event_id);
$stmt->execute();
$stmt->fetch();
$stmt->close();
return $event_id;
}
function register_event($body) {
if ($this->get_event_id($body->event_token)) {
return ["success" => false];
}
$query = "INSERT INTO events (token) VALUES (?)";
$stmt = $this->mysqli->prepare($query);
$stmt->bind_param("s", $body->event_token);
$stmt->execute();
$stmt->close();
return ["success" => true];
}
function create_hash(int $event_id, int $vote_round, string $name) : string {
// sanitize the inputs which will go to the shell
$vote_round = (int)$vote_round;
$name = addslashes($name);
// generate the hash (the PHP password_hash function does not provide the required options)
$hash = shell_exec("echo -n '$name' | argon2 'Abstimmung $vote_round' \
-p {$this->cfg["argon2"]["threads"]} \
-m {$this->cfg["argon2"]["memory"]} \
-t {$this->cfg["argon2"]["iterations"]} \
-l {$this->cfg["argon2"]["length"]} -id -r");
$hash = str_replace(array("\n", "\r"), '', $hash);
// insert the new hash into the database
$query = "INSERT INTO hashes (event, vote_round, name, hash) VALUES (?, ?, ?, ?)";
$stmt = $this->mysqli->prepare($query);
$stmt->bind_param("iiss", $event_id, $vote_round, $name, $hash);
$stmt->execute();
$stmt->close();
return $hash;
}
/**
* Utility function to get a specific voting hash
* @param int $event_id The ID of the event
* @param int $vote_round The vote round
* @param string $name The name of the delegatee
* @return string The voting hash
*/
function get_hash(int $event_id, int $vote_round, string $name) : string {
$query = "SELECT hash FROM hashes WHERE event=? AND vote_round=? AND name=? LIMIT 1";
$query = 'SELECT hash FROM hashes WHERE event=? AND vote_round=? AND name=? LIMIT 1';
$stmt = $this->mysqli->prepare($query);
$stmt->bind_param('iis', $event_id, $vote_round, $name);
$stmt->bind_result($hash);
$stmt->execute();
$hash = '';
$stmt->bind_result($hash);
$stmt->fetch();
$stmt->close();
@ -91,52 +99,107 @@ class AbstimmIDd {
return $hash;
}
function export_result($body) {
if (!$this->get_event_id($body->event_token)) {
return ["success" => false];
}
$sha256 = $this->create_text_file($body);
if (strlen($this->cfg["export"]["rawpath"]) > 0) {
$filepath = "{$this->cfg["export"]["rawpath"]}/{$body->event_token}-{$body->vote_round}.json";
file_put_contents($filepath, json_encode($body));
}
return ["success" => true, "sha256" => $sha256, "signing_key" => $this->cfg["export"]["pgpkey"]];
/**
* Utility function to generate a new hash
* @param int $event_id The ID of the event
* @param int $vote_round The vote round
* @param string $name The name of the delegatee
* @return string The voting hash
*/
function create_hash(int $event_id, int $vote_round, string $name) : string {
// sanitize the inputs which will go to the shell
$vote_round = (int)$vote_round;
$name = addslashes($name);
// generate the hash (the PHP password_hash function does not provide the required options)
$hash = shell_exec("echo -n '$name' | argon2 'Abstimmung $vote_round' \
-p {$this->cfg['argon2']['threads']} \
-m {$this->cfg['argon2']['memory']} \
-t {$this->cfg['argon2']['iterations']} \
-l {$this->cfg['argon2']['length']} -id -r");
$hash = str_replace(array("\n", "\r"), '', $hash);
// insert the new hash into the database
$query = 'INSERT INTO hashes (event, vote_round, name, hash) VALUES (?, ?, ?, ?)';
$stmt = $this->mysqli->prepare($query);
$stmt->bind_param('iiss', $event_id, $vote_round, $name, $hash);
$stmt->execute();
$stmt->close();
return $hash;
}
function create_header($body) {
$header = str_replace("%t", $body->event_title, $this->cfg["export"]["header"]);
$header = str_replace("%d", date("d.m.Y"), $header);
$header = str_replace("%r", $body->vote_round, $header);
/**
* API endpoint to generate a voting result textfile and PDF
* @param mixed $body The JSON object as specified in the API documentation
* @return mixed The JSON response object
*/
function export_result($body) {
if ($this->get_event_id($body->event_token) === 0) {
return ['success' => false];
}
return <<<EOT
$sha256 = $this->create_text_file($body);
if (strlen($this->cfg['export']['rawpath']) > 0) {
$filepath = "{$this->cfg['export']['rawpath']}/{$body->event_token}-{$body->vote_round}.json";
file_put_contents($filepath, json_encode($body));
}
return ['success' => true, 'sha256' => $sha256, 'signing_key' => $this->cfg['export']['pgpkey']];
}
/**
* Utility function to create a signed voting result text file
* @param mixed $body The complete JSON object from the API endpoint
* @return string The SHA256 hash of the file created
*/
function create_text_file($body) : string {
// sanitize the inputs which will go to the shell
$vote_round = (int)$body->vote_round;
$event_title = str_replace(' ', '_', addslashes($body->event_title));
$file_path = $this->cfg['export']['path'] . '/' . date('Y-m-d') .
"__{$event_title}__Abstimmung-$vote_round.txt";
// create the file's header
$header = str_replace('%t', $body->event_title, $this->cfg['export']['header']);
$header = str_replace('%d', date('d.m.Y'), $header);
$header = str_replace('%r', $body->vote_round, $header);
$file_data = <<<EOT
$header\n
Abstimm-ID | Stimme(n)
#################################|#############\n
EOT;
}
function create_text_file($body) {
$file_path = $this->cfg["export"]["path"] . "/" . date('Y-m-d') . "__" .
str_replace(" ", "_", $body->event_title) . "__Abstimmung-" . $body->vote_round . ".txt";
$file_data = $this->create_header($body);
// append the votes
foreach ($body->votes as $vote) {
$file_data .= $vote->hash . " | " . implode(", ", $vote->vote) . "\n";
$file_data .= $vote->hash . ' | ' . implode(', ', $vote->vote) . "\n";
}
$file_data .= "\n";
file_put_contents($file_path, $file_data);
// write the file and create a detached GPG signature
file_put_contents($file_path, $file_data);
shell_exec("gpg --sign $file_path");
// synchronize both files
$this->sync_command($file_path);
$this->sync_command("$file_path.gpg");
// return the hash of the text file
return trim(shell_exec("/usr/bin/sha256sum $file_path | awk '{ print $1 }'"));
}
function sync_command($file_path) {
if (isset($this->cfg["export"]["synccmd"])) {
$cmd = $this->cfg["export"]["synccmd"];
/**
* Utility function to synchronize a specific file with a remote location,
* with a command specified in the config.ini file
* @param string $file_path The path to the file
* @return none
*/
function sync_command(string $file_path) {
if (isset($this->cfg['export']['synccmd'])) {
$cmd = $this->cfg['export']['synccmd'];
if (strlen($cmd) > 0) {
$cmd = str_replace("%s", $file_path . ".gpg", $cmd);
$cmd = str_replace("%u", $file_path, $cmd);
$cmd = str_replace('%f', $file_path, $cmd);
shell_exec($cmd);
}
}

View File

@ -6,7 +6,9 @@ header('Content-Type: application/json');
try {
$aidd = new AbstimmIDd();
$body = json_decode(file_get_contents('php://input'));
if (($body = json_decode(file_get_contents('php://input'))) === null)
throw new Exception("invalid JSON");
if(ends_with($_SERVER["REQUEST_URI"], '/get_ids')) {
$data = $aidd->get_voting_ids($body);
@ -15,7 +17,7 @@ try {
} else if(ends_with($_SERVER["REQUEST_URI"], '/export_result')) {
$data = $aidd->export_result($body);
} else {
$data = ["error" => "no route"];
throw new Exception("no route");
}
echo json_encode($data);