This repository has been archived on 2023-07-12. You can view files and clone it, but cannot push or open issues or pull requests.
machines-serv/index.php

559 lines
15 KiB
PHP
Raw Normal View History

<?php
require __DIR__ . '/vendor/autoload.php';
use OTPHP\TOTP;
include_once('config.inc.php');
$route = explode('/', trim(substr(explode('?', $_SERVER['REDIRECT_URL'])[0], strrpos($_SERVER['SCRIPT_NAME'], '/')), '/'));
$meth = $_SERVER['REQUEST_METHOD'];
$DOMAIN_NAME_REGEX = '[a-zA-Z0-9\p{L}][a-zA-Z0-9\p{L}-\.]{1,61}[a-zA-Z0-9\p{L}]\.[a-zA-Z0-9\p{L}][a-zA-Z\p{L}-]*[a-zA-Z0-9\p{L}]+'; // From http://stackoverflow.com/a/38477788/2766106
$SSH_KEY_REGEX = '/^(ssh-(rsa|ed25519|dss)|ecdsa-sha2-nistp256) [a-zA-Z0-9+=\/]+/';
$machineArgs = array(
'name' => array(
'type' => 'string',
'pattern' => '/^[a-zA-Z0-9\-_]+$/',
'repeatable' => false,
'optional' => false
),
'host' => array(
'type' => 'string',
'pattern' => '/^'.$DOMAIN_NAME_REGEX.'(:\d+)?$/',
'repeatable' => true,
'optional' => true
),
'user' => array(
'type' => 'string',
'pattern' => '/^[a-zA-Z0-9\-_]+$/',
'repeatable' => false,
'optional' => false
),
'network' => array(
'type' => 'string',
'oneof' => 'network',
'repeatable' => false,
'optional' => true
),
'userkey' => array(
'type' => 'string',
'pattern' => $SSH_KEY_REGEX,
'repeatable' => false,
'optional' => true
),
'hostkey' => array(
'type' => 'string',
'pattern' => $SSH_KEY_REGEX,
'repeatable' => false,
'optional' => true
)
);
$networkArgs = array(
'name' => array(
'type' => 'string',
'pattern' => '/^[a-zA-Z0-9]+$/',
'repeatable' => false,
'optional' => false
),
'allowed' => array(
'type' => 'string',
'oneof' => 'network',
'repeatable' => true,
'optional' => true
),
'secure' => array(
'type' => 'boolean',
'repeatable' => false,
'optional' => false
)
);
// Compute a diff between two arrays
// From http://stackoverflow.com/a/22021254/2766106
function computeDiff($from, $to)
{
$diffValues = array();
$diffMask = array();
$dm = array();
$n1 = count($from);
$n2 = count($to);
for ($j = -1; $j < $n2; $j++) $dm[-1][$j] = 0;
for ($i = -1; $i < $n1; $i++) $dm[$i][-1] = 0;
for ($i = 0; $i < $n1; $i++)
{
for ($j = 0; $j < $n2; $j++)
{
if ($from[$i] == $to[$j])
{
$ad = $dm[$i - 1][$j - 1];
$dm[$i][$j] = $ad + 1;
}
else
{
$a1 = $dm[$i - 1][$j];
$a2 = $dm[$i][$j - 1];
$dm[$i][$j] = max($a1, $a2);
}
}
}
$i = $n1 - 1;
$j = $n2 - 1;
while (($i > -1) || ($j > -1))
{
if ($j > -1)
{
if ($dm[$i][$j - 1] == $dm[$i][$j])
{
$diffValues[] = $to[$j];
$diffMask[] = 1;
$j--;
continue;
}
}
if ($i > -1)
{
if ($dm[$i - 1][$j] == $dm[$i][$j])
{
$diffValues[] = $from[$i];
$diffMask[] = -1;
$i--;
continue;
}
}
{
$diffValues[] = $from[$i];
$diffMask[] = 0;
$i--;
$j--;
}
}
$diffValues = array_reverse($diffValues);
$diffMask = array_reverse($diffMask);
return array('values' => $diffValues, 'mask' => $diffMask);
}
function requireToken() {
global $TOTP;
if (isset($_SERVER['HTTP_X_TOTP']) && $_SERVER['HTTP_X_TOTP']) {
if (!$TOTP->verify($_SERVER['HTTP_X_TOTP'], null, 1)) {
http_response_code(403);
die("Invalid token\n");
}
} else {
http_response_code(401);
die("Authorization required\n");
}
}
function requireSigned() {
if ($_SERVER['SSL_CLIENT_VERIFY'] == 'NONE') {
http_response_code(401);
die("Authorization required\n");
} elseif ($_SERVER['SSL_CLIENT_VERIFY'] != 'SUCCESS') {
http_response_code(403);
die("Wrong certificate\n");
}
}
function logActivity($text) {
$ex = explode("\n", $text);
$t = '';
foreach ($ex as $line) {
if ($line == '') {
continue;
}
$t .= strval(time()).'|'.$line."\n";
}
file_put_contents('machines.log', $t, FILE_APPEND);
}
function save($elname, $elements) {
file_put_contents($elname.'.ser.db', serialize($elements));
}
function load($elname) {
if (!file_exists($elname.'.ser.db')) {
save($elname, []);
}
return unserialize(file_get_contents($elname.'.ser.db'));
}
function getKeys($network) {
global $SSH_KEY_REGEX;
global $DOMAIN;
$machines = load('machine');
$networks = load('networks');
$t = '';
foreach ($machines as $machineName => $machine) {
if (
(isset($machine['network']) && isset($networks[$machine['network']]) && $networks[$machine['network']]['secure'] == 'true')
|| ($network && in_array($machine['network'], $network['allowed']))
) {
if (isset($machine['userkey']) && preg_match($SSH_KEY_REGEX, $machine['userkey'], $matches)) {
$t .= $matches[0].' '.$machineName.'@'.($machine['network'] ? $machine['network'].'.' : '').$DOMAIN."\n";
}
}
}
return $t;
}
// Display an element in a simple, easily-editable,
// easily-diffable, plain text format
function displayElement($element, $elementArgs) {
$t = '';
ksort($element);
foreach ($element as $arg => $value) {
if ($arg == 'name') {
continue;
}
if (array_key_exists($arg, $elementArgs)) {
if ($value) {
$argData = $elementArgs[$arg];
if ($argData['repeatable']) {
sort($value);
foreach ($value as $arrValue) {
$t .= $arg.'[]='.$arrValue."\n";
}
} else {
$t .= $arg.'='.$value."\n";
}
}
}
}
return $t;
}
// Verifiy if one argument is valid
// return false if valid, string with
// description if not
function argAssertOne($argData, $data) {
// Type casting
switch ($argData['type']) {
case 'boolean':
switch ($data) {
case 'true':
$data = true;
break;
case 'false':
$data = false;
break;
}
break;
}
// Type validation
if (gettype($data) != $argData['type']) {
return "should be of type ".$argData['type'];
}
// Pattern validation
if (isset($argData['pattern']) && !preg_match($argData['pattern'], $data)) {
return "should match pattern ".$argData['pattern'];
}
// OneOf validation
if (isset($argData['oneof'])) {
$elname = $argData['oneof'];
$elements = load($elname);
if (!isset($elements[$data])) {
return "should be an existing $elname";
}
}
return false;
}
function argAssert($arg, $data, $args) {
$argData = $args[$arg];
# TODO oneof
if (!$data) {
if ($argData['optional']) {
return false;
} else {
return "Argument $arg is required";
}
}
if ($argData['repeatable']) {
if (gettype($data) == 'array') {
foreach ($data as $key => $dat) {
if ($err = argAssertOne($argData, $dat)) {
return "Argument $arg"."[$key] $err";
}
}
} else {
return "Argument $arg should be an array";
}
} else {
if ($err = argAssertOne($argData, $data)) {
return "Argument $arg ".$err;
}
}
return false;
}
switch ($route[0]) {
case 'machine':
case 'network':
switch ($elname = $route[0]) {
case 'machine':
$elementArgs = $machineArgs;
break;
case 'network':
$elementArgs = $networkArgs;
break;
}
// GET /element
if (count($route) == 1 && $meth == 'GET') {
requireSigned();
$elements = load($elname);
foreach ($elements as $key => $value) {
echo $key."\n";
}
// GET /element/{name}
} elseif (count($route) == 2 && $meth == 'GET') {
if ($elname != 'machine') {
requireSigned();
}
$elements = load($elname);
if (isset($elements[$route[1]])) {
$element = $elements[$route[1]];
echo displayElement($element, $elementArgs);
} else {
http_response_code(404);
die("Unknown $elname\n");
}
// POST /element
} elseif (count($route) == 1 && $meth == 'POST') {
if (isset($_SERVER['HTTP_X_TOTP']) && $elname == 'machine') {
$restrained = true;
requireToken();
} else {
$restrained = false;
requireSigned();
}
foreach ($_POST as $arg => $value) {
if ($restrained && $arg == 'network') {
http_response_code(403);
die("You can't add a network authenticated like that\n");
}
if (!array_key_exists($arg, $elementArgs)) {
die("Unknown argument $arg\n");
}
}
$elements = load($elname);
$element = array();
foreach ($elementArgs as $arg => $argData) {
if (!isset($_POST[$arg])) {
$_POST[$arg] = null;
}
if ($err = argAssert($arg, $_POST[$arg], $elementArgs)) {
http_response_code(400);
die("$err\n");
}
if ($arg == 'name') {
$name = $_POST[$arg];
} else {
$element[$arg] = $_POST[$arg];
}
}
if (isset($elements[$name])) {
http_response_code(409);
die("$elname with the same name already exists\n");
} else {
$elements[$name] = $element;
save($elname, $elements);
http_response_code(201);
logActivity("Created $elname \"$name\"\n".displayElement($element, $elementArgs));
}
// POST /element/{name}
} elseif (count($route) == 2 && $meth == 'POST') {
requireSigned();
foreach ($_POST as $arg => $value) {
if (!array_key_exists($arg, $elementArgs)) {
die("Unknown argument $arg\n");
}
}
$elements = load($elname);
if (isset($elements[$route[1]])) {
$element = $elements[$route[1]];
foreach ($elementArgs as $arg => $typ) {
if (isset($_POST[$arg])) {
if ($arg == 'name') {
http_response_code(501);
die("Can't change name\n");
}
if ($err = argAssert($arg, $_POST[$arg], $elementArgs)) {
http_response_code(400);
die("$err\n");
}
$element[$arg] = $_POST[$arg];
}
}
$oldEls = explode("\n", displayElement($elements[$route[1]], $elementArgs));
$elements[$route[1]] = $element;
save($elname, $elements);
http_response_code(204);
$newEls = explode("\n", displayElement($elements[$route[1]], $elementArgs));
$diff = computeDiff($oldEls, $newEls);
$t = '';
foreach ($diff['values'] as $i => $line) {
switch ($diff['mask'][$i]) {
case -1:
$t .= '- '.$line."\n";
break;
case 1:
$t .= '+ '.$line."\n";
break;
}
}
logActivity("Modified $elname \"$route[1]\"\n".$t);
} else {
http_response_code(404);
die("Unknown $elname\n");
}
// DELETE /element/{name}
} elseif (count($route) == 2 && $meth == 'DELETE') {
requireSigned();
$elements = load($elname);
if (isset($elements[$route[1]])) {
unset($elements[$route[1]]);
save($elname, $elements);
http_response_code(204);
logActivity("Removed $elname \"$name\"");
} else {
http_response_code(404);
die("Unknown $elname\n");
}
} else {
http_response_code(501);
die("Unkown route\n");
}
break;
// Authorized keys for networks
case 'akey':
// GET /akey/{network}
if ((count($route) == 1 || count($route) == 2) && $meth == 'GET') {
if (count($route) == 2) {
$networkName = $route[1];
} else {
$networkName = '';
}
$networks = load('network');
if (!$networkName || isset($networks[$networkName])) {
if ($networkName) {
$network = $networks[$networkName];
}
// GET /akey/{network}?signature
if (isset($_GET['signature'])) {
echo file_get_contents('akey/'.$networkName.'.authorized_keys.sha256');
// GET /akey/{network}?unsigned
} elseif (isset($_GET['unsigned'])) {
requireSigned();
echo getKeys($networkName ? $network : null);
// GET /akey/{network}
} else {
echo file_get_contents('akey/'.$networkName.'.authorized_keys');
}
} else {
http_response_code(404);
die("Unknown network\n");
}
// PUT /akey/{network}
} elseif ((count($route) == 1 || count($route) == 2) && $meth == 'PUT') {
requireSigned();
if (count($route) == 2) {
$networkName = $route[1];
} else {
$networkName = '';
}
$networks = load('network');
if (!$networkName || isset($networks[$networkName])) {
if ($networkName) {
$network = $networks[$networkName];
}
$sign = file_get_contents('php://input');
file_put_contents('akey/'.$networkName.'.authorized_keys', getKeys($networkName ? $network : null));
file_put_contents('akey/'.$networkName.'.authorized_keys.sha256', $sign);
http_response_code(201);
logActivity('Updated key '.$networkName);
} else {
http_response_code(404);
die("Unknown network\n");
}
} else {
http_response_code(501);
die("Unkown route\n");
}
break;
// Activity log
case 'log':
if (count($route) == 1 && $meth == 'GET') {
requireSigned();
if (!isset($_GET['from']) || !$from = intval($_GET['from'])) {
$from = 0;
}
$file = fopen('machines.log', 'r');
while (!feof($file)) {
$line = fgets($file);
if ($line == '') {
continue;
}
preg_match('/^(\d+)\|(.+)$/', $line, $matches);
if (intval($matches[1]) > $from) {
echo $matches[2]."\n";
}
}
fclose($file);
}
break;
case 'totp':
global $TOTP;
if (count($route) == 1 && $meth == 'GET') {
requireSigned();
echo $TOTP->getProvisioningUri();
}
break;
case 'cert':
if (count($route) == 1 && $meth == 'GET') {
echo file_get_contents('machines.crt');
}
break;
default:
http_response_code(501);
die("Unkown route\n");
}
?>