View source
<?php
namespace Drupal\drd_agent\Agent\Action;
use Drupal\Component\Datetime\Time;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Session\AccountSwitcherInterface;
use Drupal\Core\State\StateInterface;
use Drupal\drd_agent\Agent\Auth\BaseInterface as AuthBaseInterface;
use Drupal\drd_agent\Agent\Auth\Base as AuthBase;
use Drupal\drd_agent\Crypt\Base as CryptBase;
use Drupal\drd_agent\Crypt\BaseMethodInterface;
use Drupal\user\Entity\User;
use Exception;
use Psr\Log\LogLevel;
use RuntimeException;
use Symfony\Component\DependencyInjection\ContainerInterface;
class Base implements BaseInterface, ContainerInjectionInterface {
private $debugMode = FALSE;
private $arguments = array();
const SEC_AUTH_ACQUIA = 'Acquia';
const SEC_AUTH_PANTHEON = 'Pantheon';
const SEC_AUTH_PLATFORMSH = 'PlatformSH';
protected $crypt;
protected $accountSwitcher;
protected $configFactory;
protected $container;
protected $database;
protected $entityTypeManager;
protected $fileSystem;
protected $logger;
protected $messenger;
protected $moduleHandler;
protected $state;
protected $time;
public function __construct(ContainerInterface $container, AccountSwitcherInterface $account_switcher, ConfigFactoryInterface $config_factory, Connection $database, EntityTypeManagerInterface $entity_type_manager, FileSystemInterface $file_system, LoggerChannelFactoryInterface $logger_channel_factory, MessengerInterface $messenger, ModuleHandlerInterface $module_handler, StateInterface $state, Time $time) {
$this->accountSwitcher = $account_switcher;
$this->configFactory = $config_factory;
$this->container = $container;
$this->database = $database;
$this->entityTypeManager = $entity_type_manager;
$this->fileSystem = $file_system;
$this->logger = $logger_channel_factory
->get('DRD Agent');
$this->messenger = $messenger;
$this->moduleHandler = $module_handler;
$this->state = $state;
$this->time = $time;
}
public static function create(ContainerInterface $container) {
return new static($container, $container
->get('account_switcher'), $container
->get('config.factory'), $container
->get('database'), $container
->get('entity_type.manager'), $container
->get('file_system'), $container
->get('logger.factory'), $container
->get('messenger'), $container
->get('module_handler'), $container
->get('state'), $container
->get('datetime.time'));
}
private function toArray($items) {
foreach ($items as $key => $item) {
if (is_object($item)) {
$items[$key] = $this
->toArray((array) $item);
}
elseif (is_array($item)) {
$items[$key] = $this
->toArray($item);
}
}
return $items;
}
private function readInput($debugMode, $message = NULL) : array {
$this
->setDebugMode($debugMode);
if (isset($message)) {
$this
->watchdog($message, array(), 4);
}
$raw_input = file_get_contents('php://input');
if (empty($raw_input)) {
throw new RuntimeException('Can not read input');
}
$input = json_decode(base64_decode($raw_input), TRUE);
if (!is_array($input) || empty($input)) {
throw new RuntimeException('Input is empty');
}
return $input;
}
public function run($debugMode = FALSE) {
try {
$input = $this
->readInput($debugMode);
if (empty($input['uuid']) || empty($input['args']) || !isset($input['iv'])) {
throw new RuntimeException('Input is incomplete');
}
$input['args'] = base64_decode($input['args']);
$input['iv'] = base64_decode($input['iv']);
if (!empty($input['ott']) && !empty($input['config'])) {
if (!$this
->ott($input['ott'], $input['config'])) {
throw new RuntimeException('OTT config failed');
}
return 'ok';
}
if (!empty($input['auth']) && !empty($input['authsetting'])) {
$this
->authenticate($input['uuid'], $input);
}
$this->crypt = $this
->getCryptInstance($input['uuid']);
if (!$this->crypt) {
throw new RuntimeException('Encryption method not available or unauthorised');
}
$args = $this
->toArray($this->crypt
->decrypt($input['args'], $input['iv']));
if (empty($args['auth']) || !isset($args['authsetting']) || empty($args['action'])) {
throw new RuntimeException('Arguments incomplete');
}
if (empty($input['auth'])) {
$this
->authenticate($input['uuid'], $args);
}
$action = $args['action'];
$actionModule = $args['drd_action_module'];
if ($actionModule === 'drd') {
$actionModule = 'drd_agent';
}
if (isset($args['drd_action_plugin'])) {
$actionFile = $this
->realPath('temporary://drd_agent_' . $action . '.php');
file_put_contents($actionFile, $args['drd_action_plugin']);
unset($args['drd_action_plugin']);
require_once $actionFile;
}
unset($args['auth'], $args['authsetting'], $args['action'], $args['drd_action_module']);
$this->arguments = $args;
} catch (Exception $ex) {
$this
->watchdog($ex
->getMessage(), array(), 3);
header('HTTP/1.1 502 Error');
print 'error';
exit;
}
try {
$this
->promoteUser();
$classname = "\\Drupal\\{$actionModule}\\Agent\\Action\\{$action}";
$actionObject = $classname::create($this->container);
$actionObject
->init($this->crypt, $this->arguments, $this->debugMode);
} catch (Exception $ex) {
$this
->watchdog('Not yet implemented: ' . $action, array(), 3);
header('HTTP/1.1 403 Not found');
print 'Not yet implemented';
exit;
}
$result = $actionObject
->execute();
if (is_array($result)) {
$result['messages'] = $this
->getMessages();
return base64_encode($this->crypt
->encrypt($result));
}
return $result;
}
private function authenticate($uuid, array $args) : self {
$auth_methods = AuthBase::getMethods($this->container);
if (!isset($auth_methods[$args['auth']]) || !$auth_methods[$args['auth']] instanceof AuthBaseInterface) {
throw new RuntimeException('Unrecognized authentication method');
}
$auth = $auth_methods[$args['auth']];
if (!$auth
->validateUuid($uuid)) {
throw new RuntimeException('DRD instance not registered');
}
if (!$auth
->validate($args['authsetting'])) {
throw new RuntimeException('Not authenticated');
}
return $this;
}
public function authorizeBySecret($debugMode = FALSE) : string {
try {
$input = $this
->readInput($debugMode, 'Authorize DRD by secret');
if (empty($input['remoteSetupToken']) || empty($input['method']) || empty($input['secrets'])) {
throw new RuntimeException('Input is incomplete');
}
switch ($input['method']) {
case self::SEC_AUTH_ACQUIA:
$required = array(
'username',
'password',
);
$local = $this
->getDbInfo();
break;
case self::SEC_AUTH_PANTHEON:
$required = array(
'PANTHEON_SITE',
);
$local = $_ENV;
break;
case self::SEC_AUTH_PLATFORMSH:
$required = array(
'PLATFORM_PROJECT',
);
$local = $_ENV;
break;
default:
throw new RuntimeException('Unknown method.');
}
foreach ($required as $item) {
if (!isset($local[$item])) {
throw new RuntimeException('Unsupported method.');
}
if ($local[$item] !== $input['secrets'][$item]) {
throw new RuntimeException('Invalid secret.');
}
}
$this
->authorize($input['remoteSetupToken']);
} catch (Exception $ex) {
$this
->watchdog($ex
->getMessage(), array(), 3);
sleep(10);
header('HTTP/1.1 502 Error');
print 'error';
exit;
}
return 'ok';
}
public function getArguments() : array {
return $this->arguments;
}
public function getDebugMode() : bool {
return $this->debugMode;
}
public function setDebugMode($debugMode) : BaseInterface {
$this->debugMode = $debugMode;
return $this;
}
public function promoteUser() : BaseInterface {
global $user;
$user = User::load(1);
return $this;
}
public function getCryptInstance($uuid) {
$config = $this->configFactory
->get('drd_agent.settings');
$authorised = $config
->get('authorised') ?? [];
if (empty($authorised[$uuid])) {
return FALSE;
}
return CryptBase::getInstance($this->container, $authorised[$uuid]['crypt'], (array) $authorised[$uuid]['cryptsetting']);
}
public function authorize($remoteSetupToken) : BaseInterface {
$service = $this->container
->get('drd_agent.setup');
$service
->setRemoteSetupToken($remoteSetupToken)
->execute();
return $this;
}
public function getDbInfo() : array {
return $this->database
->getConnectionOptions();
}
public function watchdog($message, array $variables = [], $severity = 5, $link = NULL) : BaseInterface {
if ($this
->getDebugMode()) {
if ($link) {
$variables['link'] = $link;
}
$this->logger
->log($severity, $message, $variables);
}
return $this;
}
public function ott($token, $remoteSetupToken) : bool {
$config = $this->configFactory
->getEditable('drd_agent.settings');
$ott = $config
->get('ott');
if (!$ott) {
$this
->watchdog('No OTT available', [], LogLevel::ERROR);
return FALSE;
}
$config
->clear('ott');
if (empty($ott['expires']) || $ott['expires'] < $this->time
->getRequestTime()) {
$this
->watchdog('OTT expired', [], LogLevel::ERROR);
return FALSE;
}
if (empty($ott['token']) || $ott['token'] !== $token) {
$this
->watchdog('Token missmatch: :local / :remote', [
':local' => $ott['token'],
':remote' => $token,
], LogLevel::ERROR);
return FALSE;
}
$service = $this->container
->get('drd_agent.setup');
$service
->setRemoteSetupToken($remoteSetupToken)
->execute();
$this
->watchdog('OTT config completed', [], LogLevel::INFO);
return TRUE;
}
public function realPath($path) : string {
return $this->fileSystem
->realpath($path);
}
public function getMessages() : array {
return $this->messenger
->all();
}
public function execute() {
}
public function init(BaseMethodInterface $crypt, array $arguments, $debugMode) {
$this->crypt = $crypt;
$this->arguments = $arguments;
$this->debugMode = $debugMode;
}
}