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
Geoffrey Frogeye 931a6ae2b3 Initial commit
Well, here we are. New set of scripts that is meant to improve security
of my machines by using an unique SSH key per host and not to carry
them using .dotfile synchronisation. Might be even less secure than
before if there are any flaw, that's why I kept this repository secret for now.
2016-12-10 18:02:14 +01:00

559 lines
15 KiB
PHP

<?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");
}
?>