2016-12-10 18:02:14 +01:00
< ? php
require __DIR__ . '/vendor/autoload.php' ;
2016-12-10 20:16:23 +01:00
require_once ( 'config.inc.php' );
2016-12-10 18:02:14 +01:00
$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' );
2016-12-10 20:16:23 +01:00
$networks = load ( 'network' );
2016-12-10 18:02:14 +01:00
$t = '' ;
foreach ( $machines as $machineName => $machine ) {
if (
2016-12-10 20:16:23 +01:00
(
isset ( $machine [ 'network' ])
&& isset ( $networks [ $machine [ 'network' ]])
) && (
(
$networks [ $machine [ 'network' ]][ 'secure' ] == 'true'
) || (
$network
&& isset ( $network [ 'allowed' ])
&& in_array ( $machine [ 'network' ], $network [ 'allowed' ])
)
)
2016-12-10 18:02:14 +01:00
) {
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 ;
}
2016-12-10 21:44:21 +01:00
// Hooks
//
function gogsRequest ( $route , $meth = 'GET' , $data = null ) {
global $GOGS_API ;
global $GOGS_TOKEN ;
$ch = curl_init ();
curl_setopt ( $ch , CURLOPT_URL , $GOGS_API . '/' . $route );
curl_setopt ( $ch , CURLOPT_HTTPHEADER , array ( 'Authorization: token ' . $GOGS_TOKEN ));
curl_setopt ( $ch , CURLOPT_CUSTOMREQUEST , $meth );
if ( $data ) {
$textdata = '' ;
foreach ( $data as $key => $value ) {
$textdata .= '&' . $key . '=' . urlencode ( $value );
}
curl_setopt ( $ch , CURLOPT_POST , count ( $data ));
curl_setopt ( $ch , CURLOPT_POSTFIELDS , ltrim ( $textdata , '&' ));
}
curl_setopt ( $ch , CURLOPT_RETURNTRANSFER , true );
$raw = curl_exec ( $ch );
curl_close ( $ch );
return json_decode ( $raw );
}
function updateGogsKeys ( $keys ) {
global $GOGS_API ;
global $GOGS_TOKEN ;
global $SSH_KEY_REGEX ;
if ( isset ( $GOGS_API ) && isset ( $GOGS_TOKEN )) {
$existing = gogsRequest ( 'user/keys' );
$toDelete = [];
foreach ( $existing as $ekey ) {
$toDelete [ $ekey -> id ] = $ekey -> key ;
}
foreach ( explode ( " \n " , $keys ) as $key ) {
$found = false ;
foreach ( $toDelete as $id => $ekey ) {
if ( $key == $ekey ) {
unset ( $toDelete [ $id ]);
$found = true ;
break ;
}
}
if ( ! $found ) {
gogsRequest ( 'user/keys' , 'POST' , array (
" title " => ltrim ( preg_replace ( $SSH_KEY_REGEX , '' , $key )),
" key " => $key
));
}
}
foreach ( $toDelete as $id => $ekey ) {
gogsRequest ( 'user/keys/' . $id , 'DELETE' );
}
}
}
2016-12-10 18:02:14 +01:00
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 );
2016-12-10 21:44:21 +01:00
if ( $networkName == 'gogs' ) {
updateGogsKeys ( getKeys ( $network ));
}
2016-12-10 18:02:14 +01:00
http_response_code ( 201 );
2016-12-10 21:44:21 +01:00
logActivity ( 'Updated akeys ' . $networkName );
2016-12-10 18:02:14 +01:00
} 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 " );
}
?>