Page MenuHomeWickedGov Phorge

Server.php
No OneTemporary

Size
9 KB
Referenced Files
None
Subscribers
None

Server.php

<?php
namespace Shellbox;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Uri;
use Monolog\Formatter\JsonFormatter;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogHandler;
use Monolog\Logger;
use Monolog\Processor\PsrLogMessageProcessor;
use Shellbox\Action\CallAction;
use Shellbox\Action\ShellAction;
use Throwable;
/**
* The Shellbox server main class
*
* To use this, create a PHP entry point file with:
*
* require __DIR__ . '/vendor/autoload.php';
* Shellbox\Server::main();
*
*/
class Server {
/** @var array */
private $config;
/** @var bool[] */
private $forgottenConfig = [];
/** @var Logger */
private $logger;
/** @var ClientLogHandler|null */
private $clientLogHandler;
private const DEFAULT_CONFIG = [
'allowedActions' => [ 'call', 'shell' ],
'allowedRoutes' => null,
'routeSpecs' => [],
'useSystemd' => null,
'useBashWrapper' => null,
'useFirejail' => null,
'firejailPath' => null,
'firejailProfile' => null,
'logFile' => false,
'jsonLogFile' => false,
'logToStderr' => false,
'jsonLogToStderr' => false,
'syslogIdent' => 'shellbox',
'logToSyslog' => false,
'logToClient' => true,
'logFormat' => LineFormatter::SIMPLE_FORMAT,
'allowUrlFiles' => false,
'urlFileConcurrency' => 5,
'urlFileConnectTimeout' => 3,
'urlFileRequestTimeout' => 600,
'urlFileUploadAttempts' => 3,
'urlFileRetryDelay' => 1,
];
/**
* The main entry point. Call this from the webserver.
*
* @param string|null $configPath The location of the JSON config file
*/
public static function main( $configPath = null ) {
( new self )->execute( $configPath );
}
/**
* Non-static entry point
*
* @param string|null $configPath
*/
protected function execute( $configPath ) {
set_error_handler( [ $this, 'handleError' ] );
try {
$this->guardedExecute( $configPath );
} catch ( Throwable $e ) {
$this->handleException( $e );
} finally {
set_error_handler( null );
}
}
/**
* Entry point that may throw exceptions
*
* @param string|null $configPath
*/
private function guardedExecute( $configPath ) {
$this->setupConfig( $configPath );
$this->setupLogger();
$url = $_SERVER['REQUEST_URI'];
$base = ( new Uri( $this->getConfig( 'url' ) ) )->getPath();
if ( $base[-1] !== '/' ) {
$base .= '/';
}
if ( substr_compare( $url, $base, 0, strlen( $base ) ) !== 0 ) {
throw new ShellboxError( "Request URL does not match configured base path", 404 );
}
$baseLength = strlen( $base );
$pathInfo = substr( $url, $baseLength );
$components = explode( '/', $pathInfo );
$action = array_shift( $components );
if ( $action === '' ) {
throw new ShellboxError( "No action was specified" );
}
if ( $action === 'healthz' ) {
$this->showHealth();
return;
} elseif ( $action === 'spec' ) {
$this->showSpec();
return;
}
if ( $this->validateAction( $action ) ) {
switch ( $action ) {
case 'call':
$handler = new CallAction( $this );
break;
case 'shell':
$handler = new ShellAction( $this );
break;
default:
throw new ShellboxError( "Unknown action: $action" );
}
} else {
throw new ShellboxError( "Invalid action: $action" );
}
$handler->setLogger( $this->logger );
$handler->baseExecute( $components );
}
/**
* Read the configuration file into $this->config
*
* @param string|null $configPath
*/
private function setupConfig( $configPath ) {
if ( $configPath === null ) {
$configPath = $_ENV['SHELLBOX_CONFIG_PATH'] ?? '';
if ( $configPath === '' ) {
$configPath = __DIR__ . '/../shellbox-config.json';
}
}
$json = file_get_contents( $configPath );
if ( $json === false ) {
throw new ShellboxError( 'This entry point is disabled: ' .
"the configuration file $configPath is not present" );
}
$config = json_decode( $json, true );
if ( $config === null ) {
throw new ShellboxError( 'Error parsing JSON config file' );
}
$key = $_ENV['SHELLBOX_SECRET_KEY'] ?? $_SERVER['SHELLBOX_SECRET_KEY'] ?? '';
if ( $key !== '' ) {
if ( isset( $config['secretKey'] )
&& $key !== $config['secretKey']
) {
throw new ShellboxError( 'The SHELLBOX_SECRET_KEY server ' .
'variable conflicts with the secretKey configuration' );
}
// Attempt to hide the key from code running later in the same request.
// I think this could be made to be secure in plain CGI and
// apache2handler, but it doesn't work in FastCGI or FPM modes.
if ( function_exists( 'apache_setenv' ) ) {
apache_setenv( 'SHELLBOX_SECRET_KEY', '' );
}
$_SERVER['SHELLBOX_SECRET_KEY'] = '';
$_ENV['SHELLBOX_SECRET_KEY'] = '';
putenv( 'SHELLBOX_SECRET_KEY=' );
$config['secretKey'] = $key;
}
$this->config = $config + self::DEFAULT_CONFIG;
}
/**
* Get a configuration variable
*
* @param string $name
* @return mixed
*/
public function getConfig( $name ) {
if ( isset( $this->forgottenConfig[$name] ) ) {
throw new ShellboxError( "Access to the configuration variable \"$name\" " .
"is no longer possible" );
}
if ( !array_key_exists( $name, $this->config ) ) {
throw new ShellboxError( "The configuration variable \"$name\" is required, " .
"but it is not present in the config file." );
}
return $this->config[$name];
}
/**
* Forget a configuration variable. This is used to try to hide the HMAC
* key from code which is run by the call action.
*
* @param string $name
*/
public function forgetConfig( $name ) {
if ( isset( $this->config[$name] ) && is_string( $this->config[$name] ) ) {
$conf =& $this->config[$name];
$length = strlen( $conf );
for ( $i = 0; $i < $length; $i++ ) {
$conf[$i] = ' ';
}
unset( $conf );
}
unset( $this->config[$name] );
$this->forgottenConfig[$name] = true;
}
/**
* Set up logging based on current configuration.
*/
private function setupLogger() {
$this->logger = new Logger( 'shellbox' );
$this->logger->pushProcessor( new PsrLogMessageProcessor );
$formatter = new LineFormatter( $this->getConfig( 'logFormat' ) );
$jsonFormatter = new JsonFormatter( JsonFormatter::BATCH_MODE_NEWLINES );
if ( strlen( $this->getConfig( 'logFile' ) ) ) {
$handler = new StreamHandler( $this->getConfig( 'logFile' ) );
$handler->setFormatter( $formatter );
$this->logger->pushHandler( $handler );
}
if ( strlen( $this->getConfig( 'jsonLogFile' ) ) ) {
$handler = new StreamHandler( $this->getConfig( 'jsonLogFile' ) );
$handler->setFormatter( $jsonFormatter );
$this->logger->pushHandler( $handler );
}
if ( $this->getConfig( 'logToStderr' ) ) {
$handler = new StreamHandler( 'php://stderr' );
$handler->setFormatter( $formatter );
$this->logger->pushHandler( $handler );
}
if ( $this->getConfig( 'jsonLogToStderr' ) ) {
$handler = new StreamHandler( 'php://stderr' );
$handler->setFormatter( $jsonFormatter );
$this->logger->pushHandler( $handler );
}
if ( $this->getConfig( 'logToSyslog' ) ) {
$this->logger->pushHandler(
new SyslogHandler( $this->getConfig( 'syslogIdent' ) ) );
}
if ( $this->getConfig( 'logToClient' ) ) {
$this->clientLogHandler = new ClientLogHandler;
$this->logger->pushHandler( $this->clientLogHandler );
}
}
/**
* Check whether the action is in the list of allowed actions.
*
* @param string $action
* @return bool
*/
private function validateAction( $action ) {
$allowed = $this->getConfig( 'allowedActions' );
return in_array( $action, $allowed, true );
}
/**
* Handle an exception.
*
* @param Throwable $exception
*/
private function handleException( $exception ) {
if ( $this->logger ) {
$this->logger->error(
"Exception of class " . get_class( $exception ) . ': ' .
$exception->getMessage(),
[
'trace' => $exception->getTraceAsString()
]
);
}
if ( headers_sent() ) {
return;
}
if ( $exception->getCode() >= 300 && $exception->getCode() < 600 ) {
$code = $exception->getCode();
} else {
$code = 500;
}
$code = intval( $code );
$response = new Response( $code );
$reason = $response->getReasonPhrase();
header( "HTTP/1.1 $code $reason" );
header( 'Content-Type: application/json' );
echo Shellbox::jsonEncode( [
'__' => 'Shellbox server error',
'class' => get_class( $exception ),
'message' => $exception->getMessage(),
'log' => $this->flushLogBuffer(),
] );
}
/**
* Handle an error
* @param int $level
* @param string $message
* @param string $file
* @param int $line
* @return never
*/
public function handleError( $level, $message, $file, $line ) {
throw new ShellboxError( "PHP error in $file line $line: $message" );
}
/**
* healthz action
*/
private function showHealth() {
header( 'Content-Type: application/json' );
echo Shellbox::jsonEncode( [
'__' => 'Shellbox running',
'pid' => getmypid()
] );
}
/**
* spec action
*/
private function showSpec() {
header( 'Content-Type: application/json' );
echo file_get_contents( __DIR__ . '/spec.json' );
}
/**
* Get the buffered log entries to return to the client, and clear the
* buffer. If logToClient is false, this returns an empty array.
*
* @return array
*/
public function flushLogBuffer() {
return $this->clientLogHandler ? $this->clientLogHandler->flush() : [];
}
}

File Metadata

Mime Type
text/x-php
Expires
Fri, Jul 3, 18:07 (1 d, 4 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
29/fd/e28342eb61cd89c60b71d49ed0cc
Default Alt Text
Server.php (9 KB)

Event Timeline