diff --git a/shield.services.yml b/shield.services.yml index 8780ed0..e8cb6ac 100644 --- a/shield.services.yml +++ b/shield.services.yml @@ -1,7 +1,7 @@ services: shield.middleware: class: Drupal\shield\ShieldMiddleware - arguments: ['@config.factory'] + arguments: ['@config.factory', '@password', '@module_handler', '@entity.manager'] tags: # Ensure to come before page caching, so you don't serve cached pages to # banned users. diff --git a/src/ShieldMiddleware.php b/src/ShieldMiddleware.php index ae2f8da..520b144 100644 --- a/src/ShieldMiddleware.php +++ b/src/ShieldMiddleware.php @@ -4,6 +4,9 @@ namespace Drupal\shield; use Drupal\Component\Utility\Crypt; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Password\PasswordInterface; +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpKernelInterface; @@ -13,102 +16,157 @@ use Symfony\Component\HttpKernel\HttpKernelInterface; */ class ShieldMiddleware implements HttpKernelInterface { - /** - * The decorated kernel. - * - * @var \Symfony\Component\HttpKernel\HttpKernelInterface - */ - protected $httpKernel; - - /** - * The config factory. - * - * @var \Drupal\Core\Config\ConfigFactoryInterface - */ - protected $configFactory; - - /** - * Constructs a BanMiddleware object. - * - * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel - * The decorated kernel. - * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory - * The configuration factory. - */ - public function __construct(HttpKernelInterface $http_kernel, ConfigFactoryInterface $config_factory) { - $this->httpKernel = $http_kernel; - $this->configFactory = $config_factory; - } - - /** - * {@inheritdoc} - */ - public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) { - $config = $this->configFactory->get('shield.settings'); - $allow_cli = $config->get('allow_cli'); - - switch ($config->get('credential_provider')) { - case 'shield': - $user = $config->get('credentials.shield.user'); - $pass = $config->get('credentials.shield.pass'); - break; - case 'key': - $user = $config->get('credentials.key.user'); - - /** @var \Drupal\Core\Entity\EntityStorageInterface $storage */ - $storage = \Drupal::entityTypeManager()->getStorage('key'); - /** @var \Drupal\key\KeyInterface $pass_key */ - $pass_key = $storage->load($config->get('credentials.key.pass_key')); - if ($pass_key) { - $pass = $pass_key->getKeyValue(); - } - break; - case 'multikey': - /** @var \Drupal\Core\Entity\EntityStorageInterface $storage */ - $storage = \Drupal::entityTypeManager()->getStorage('key'); - /** @var \Drupal\key\KeyInterface $user_pass_key */ - $user_pass_key = $storage->load($config->get('credentials.multikey.user_pass_key')); - if ($user_pass_key) { - $values = $user_pass_key->getKeyValues(); - $user = $values['username']; - $pass = $values['password']; - } - break; - } - - if ($type != self::MASTER_REQUEST || !$user || (PHP_SAPI === 'cli' && $allow_cli)) { - // Bypass: - // 1. Subrequests - // 2. Empty username - // 3. CLI requests if CLI is allowed. - return $this->httpKernel->handle($request, $type, $catch); - } - else { - if ($request->server->has('PHP_AUTH_USER') && $request->server->has('PHP_AUTH_PW')) { - $input_user = $request->server->get('PHP_AUTH_USER'); - $input_pass = $request->server->get('PHP_AUTH_PW'); - } - elseif (!empty($request->server->get('HTTP_AUTHORIZATION'))) { - list($input_user, $input_pass) = explode(':', base64_decode(substr($request->server->get('HTTP_AUTHORIZATION'), 6)), 2); - } - elseif (!empty($request->server->get('REDIRECT_HTTP_AUTHORIZATION'))) { - list($input_user, $input_pass) = explode(':', base64_decode(substr($request->server->get('REDIRECT_HTTP_AUTHORIZATION'), 6)), 2); - } - - if (isset($input_user) && $input_user === $user && Crypt::hashEquals($pass, $input_pass)) { - return $this->httpKernel->handle($request, $type, $catch); - } - } - - $response = new Response(); - $response->headers->add([ - 'WWW-Authenticate' => 'Basic realm="' . strtr($config->get('print'), [ - '[user]' => $user, - '[pass]' => $pass, - ]) . '"', - ]); - $response->setStatusCode(401); - return $response; - } + /** + * The decorated kernel. + * + * @var \Symfony\Component\HttpKernel\HttpKernelInterface + */ + protected $httpKernel; + + /** + * The config factory. + * + + * @var \Drupal\Core\Config\ConfigFactoryInterface + + */ + protected $configFactory; + + /** + * The password hashing service. + * + * @var \Drupal\Core\Password\PasswordInterface + */ + protected $passwordChecker; + + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * The user storage. + * + * @var \Drupal\user\UserStorageInterface + */ + protected $userStorage; + + /** + * Constructs a BanMiddleware object. + * + * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel + * The decorated kernel. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The configuration factory. + * @param \Drupal\Core\Password\PasswordInterface $password_checker + * The password service. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager + * The entity manager. + */ + public function __construct( + HttpKernelInterface $http_kernel, + ConfigFactoryInterface $config_factory, + PasswordInterface $password_checker, + ModuleHandlerInterface $module_handler, + EntityManagerInterface $entity_manager + ) { + $this->httpKernel = $http_kernel; + $this->configFactory = $config_factory; + $this->passwordChecker = $password_checker; + $this->moduleHandler = $module_handler; + $this->userStorage = $entity_manager->getStorage('user'); + } + + /** + * {@inheritdoc} + */ + public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) { + $config = $this->configFactory->get('shield.settings'); + $allow_cli = $config->get('allow_cli'); + $basic_auth_enabled = $this->moduleHandler->moduleExists('basic_auth'); + + switch ($config->get('credential_provider')) { + case 'shield': + $user = $config->get('credentials.shield.user'); + $pass = $config->get('credentials.shield.pass'); + break; + case 'key': + $user = $config->get('credentials.key.user'); + + /** @var \Drupal\Core\Entity\EntityStorageInterface $storage */ + $storage = \Drupal::entityTypeManager()->getStorage('key'); + /** @var \Drupal\key\KeyInterface $pass_key */ + $pass_key = $storage->load($config->get('credentials.key.pass_key')); + if ($pass_key) { + $pass = $pass_key->getKeyValue(); + } + break; + case 'multikey': + /** @var \Drupal\Core\Entity\EntityStorageInterface $storage */ + $storage = \Drupal::entityTypeManager()->getStorage('key'); + /** @var \Drupal\key\KeyInterface $user_pass_key */ + $user_pass_key = $storage->load($config->get('credentials.multikey.user_pass_key')); + if ($user_pass_key) { + $values = $user_pass_key->getKeyValues(); + $user = $values['username']; + $pass = $values['password']; + } + break; + } + + if ($type != self::MASTER_REQUEST || !$user || (PHP_SAPI === 'cli' && $allow_cli)) { + // Bypass: + // 1. Subrequests + // 2. Empty username + // 3. CLI requests if CLI is allowed. + return $this->httpKernel->handle($request, $type, $catch); + } + else { + if ($request->server->has('PHP_AUTH_USER') && $request->server->has('PHP_AUTH_PW')) { + $input_user = $request->server->get('PHP_AUTH_USER'); + $input_pass = $request->server->get('PHP_AUTH_PW'); + } + elseif (!empty($request->server->get('HTTP_AUTHORIZATION'))) { + list($input_user, $input_pass) = explode(':', base64_decode(substr($request->server->get('HTTP_AUTHORIZATION'), 6)), 2); + } + elseif (!empty($request->server->get('REDIRECT_HTTP_AUTHORIZATION'))) { + list($input_user, $input_pass) = explode(':', base64_decode(substr($request->server->get('REDIRECT_HTTP_AUTHORIZATION'), 6)), 2); + } + + if (isset($input_user) && $input_user === $user && Crypt::hashEquals($pass, $input_pass)) { + // Fix: allow access after the shield authentication, + // in the case is the core module basic_auth enabled. + if ($basic_auth_enabled) { + $request->headers->set('PHP_AUTH_PW', NULL); + $request->headers->set('PHP_AUTH_USER', NULL); + } + return $this->httpKernel->handle($request, $type, $catch); + } + } + + // Fix: allow rest api web service access, + // in the case is the core module basic_auth enabled. + if ($basic_auth_enabled && !empty($input_user) && strlen($input_pass) > 0) { + // Check exist account by username + $account_search = $this->userStorage->loadByProperties(['name' => $input_user]); + if ($account = reset($account_search)) { + if ($this->passwordChecker->check($input_pass, $account->getPassword())) { + return $this->httpKernel->handle($request, $type, $catch); + } + } + } + + $response = new Response(); + $response->headers->add([ + 'WWW-Authenticate' => 'Basic realm="' . strtr($config->get('print'), [ + '[user]' => $user, + '[pass]' => $pass, + ]) . '"', + ]); + $response->setStatusCode(401); + return $response; + } }