librarymarket/msadiag

View on GitHub
src/Command/ProbeEncryptionCommand.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php

declare(strict_types = 1);

namespace LibraryMarket\msadiag\Command;

use Composer\CaBundle\CaBundle;

use LibraryMarket\msadiag\SMTP\ConnectionFactory;
use LibraryMarket\msadiag\SMTP\ConnectionFactoryInterface;
use LibraryMarket\msadiag\SMTP\ConnectionType;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * Probe the specified SMTP server for encryption information.
 */
class ProbeEncryptionCommand extends Command {

  /**
   * The SMTP connection factory.
   *
   * @var \LibraryMarket\msadiag\SMTP\ConnectionFactoryInterface
   */
  public ConnectionFactoryInterface $connectionFactory;

  /**
   * {@inheritdoc}
   */
  public function __construct(string $name = NULL, ?ConnectionFactoryInterface $connection_factory = NULL) {
    $this->connectionFactory = $connection_factory ??= new ConnectionFactory();

    parent::__construct($name);
  }

  /**
   * {@inheritdoc}
   */
  protected function configure(): void {
    $this->setName('probe:encryption');
    $this->setAliases(['crypto', 'encryption', 'pr-enc', 'pr:enc']);
    $this->setDescription('Probe the specified SMTP server for encryption information');
    $this->setHelp('This command connects to the specified SMTP server and probes it for information about its encryption support.');

    $this->addArgument('server-address', InputArgument::REQUIRED, 'The address of the SMTP server');
    $this->addArgument('server-port', InputArgument::REQUIRED, 'The port of the SMTP server');

    $this->addOption('tls', NULL, InputOption::VALUE_NONE, 'Use TLS for encryption instead of STARTTLS');
    $this->addOption('format', NULL, InputOption::VALUE_REQUIRED, 'The output format of this command (console, CSV, or JSON)', 'console');
  }

  /**
   * {@inheritdoc}
   */
  protected function execute(InputInterface $input, OutputInterface $output): int {
    try {
      $format = \strtoupper($input->getOption('format'));
      $format = match ($format) {
        'CONSOLE' => $this->printServerEncryptionConsole(...),
        'CSV' => $this->printServerEncryptionCommaSeparatedValue(...),
        'JSON' => fn ($output, $ext) => $output->writeln(\json_encode($ext)),
      };
    }
    catch (\UnhandledMatchError $e) {
      throw new \InvalidArgumentException('The supplied format is invalid: ' . $input->getOption('format'));
    }

    $connection_type = ConnectionType::STARTTLS;
    if ($input->getOption('tls')) {
      $connection_type = ConnectionType::TLS;
    }

    $address = $input->getArgument('server-address');
    $port = \intval($input->getArgument('server-port'));

    $connection = $this->connectionFactory->create($address, $port, $connection_type, \stream_context_get_default([
      'ssl' => [
        'SNI_enabled' => TRUE,
        'allow_self_signed' => TRUE,
        'cafile' => CaBundle::getBundledCaBundlePath(),
        'capath' => \dirname(CaBundle::getBundledCaBundlePath()),
        'crypto_method' => \STREAM_CRYPTO_METHOD_ANY_CLIENT,
        'disable_compression' => TRUE,
        'verify_peer' => FALSE,
        'verify_peer_name' => FALSE,
      ],
    ]));

    $connection->connect();
    $connection->probe();

    $default = [
      'protocol' => 'Unknown',
      'cipher_name' => 'Unknown',
      'cipher_bits' => 'Unknown',
      'cipher_version' => 'Unknown',
    ];

    $crypto = $connection->getMetadata()['crypto'] ?? [];
    $crypto = \array_replace($default, \array_intersect_key($crypto, $default));

    $format($output, $crypto);

    return 0;
  }

  /**
   * Print information about the remote server's encryption in a table.
   *
   * @param \Symfony\Component\Console\Output\OutputInterface $output
   *   The output interface to which the table should be rendered.
   * @param array $crypto
   *   An associative array of cryptographic information.
   *
   * @phpstan-ignore-next-line
   */
  protected function printServerEncryptionConsole(OutputInterface $output, array $crypto): void {
    $table = new Table($output);

    $table->setHeaderTitle('Encryption');
    $table->setHeaders(['Field', 'Value']);

    foreach ($crypto as $field => $value) {
      $table->addRow([$field, $value]);
    }

    $table->render();
  }

  /**
   * Print information about the remote server's encryption in CSV format.
   *
   * @param \Symfony\Component\Console\Output\OutputInterface $output
   *   The output interface to which the CSV should be rendered.
   * @param array $crypto
   *   An associative array of cryptographic information.
   *
   * @phpstan-ignore-next-line
   */
  protected function printServerEncryptionCommaSeparatedValue(OutputInterface $output, array $crypto): void {
    if (!$fh = \fopen('php://memory', 'r+')) {
      throw new \RuntimeException('Unable to create temporary buffer to generate CSV output');
    }

    \fputcsv($fh, ['Field', 'Value']);
    foreach ($crypto as $field => $value) {
      \fputcsv($fh, [$field, $value]);
    }

    \rewind($fh);

    // Write the contents of the buffer to the supplied output.
    if ($result = \stream_get_contents($fh)) {
      $output->write($result);
    }

    \fclose($fh);
  }

}