feat: implement mail-login directadmin plugin

This commit is contained in:
Marek Miklewicz
2026-06-30 11:31:19 +02:00
parent f6506e3293
commit 77c5431db0
34 changed files with 1620 additions and 0 deletions
+136
View File
@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
final class LoginUrlRequest
{
public function __construct(
public readonly string $username,
public readonly string $sourceHost,
public readonly string $serverName,
public readonly int $ttl,
public readonly bool $userIpBound,
public readonly string $clientIp,
public readonly string $redirectPath
) {
}
}
final class ProcessResult
{
public function __construct(
public readonly int $exitCode,
public readonly string $stdout,
public readonly string $stderr
) {
}
}
final class RemoteLoginUrlClient
{
/** @var callable(array<int,string>,int):ProcessResult */
private $runner;
/**
* @param null|callable(array<int,string>,int):ProcessResult $runner
*/
public function __construct(?callable $runner = null)
{
$this->runner = $runner ?? [self::class, 'runProcess'];
}
/**
* @return list<string>
*/
public static function buildCommand(Settings $settings, LoginUrlRequest $request): array
{
return [
'ssh',
'-o',
'BatchMode=yes',
'-o',
'StrictHostKeyChecking=yes',
'-o',
'UserKnownHostsFile=' . $settings->sshKnownHosts(),
'-o',
'IdentitiesOnly=yes',
'-i',
$settings->sshIdentity(),
'-p',
(string)$settings->sshPort(),
$settings->sshUser() . '@' . $settings->sshHost(),
'mail-login-create-url',
$request->username,
$request->sourceHost,
$request->serverName,
(string)$request->ttl,
$request->userIpBound ? '1' : '0',
$request->clientIp,
$request->redirectPath,
];
}
public function create(Settings $settings, LoginUrlRequest $request): string
{
$command = self::buildCommand($settings, $request);
$result = ($this->runner)($command, $settings->connectTimeout());
if (!$result instanceof ProcessResult) {
throw new RuntimeException('Invalid process runner result');
}
if ($result->exitCode !== 0) {
throw new RuntimeException('MX helper failed');
}
if (!preg_match('/https:\/\/\S+/', $result->stdout, $match)) {
throw new RuntimeException('MX helper did not return a login URL');
}
try {
return UrlValidator::assertGeneratedLoginUrl(trim($match[0]), $settings);
} catch (InvalidArgumentException $e) {
throw new RuntimeException($e->getMessage(), 0, $e);
}
}
/**
* @param list<string> $command
*/
public static function runProcess(array $command, int $timeout): ProcessResult
{
$descriptors = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$process = proc_open($command, $descriptors, $pipes);
if (!is_resource($process)) {
throw new RuntimeException('Cannot start MX helper');
}
fclose($pipes[0]);
stream_set_blocking($pipes[1], false);
stream_set_blocking($pipes[2], false);
$stdout = '';
$stderr = '';
$deadline = time() + $timeout;
do {
$stdout .= stream_get_contents($pipes[1]) ?: '';
$stderr .= stream_get_contents($pipes[2]) ?: '';
$status = proc_get_status($process);
if (!$status['running']) {
break;
}
if (time() >= $deadline) {
proc_terminate($process);
foreach ([$pipes[1], $pipes[2]] as $pipe) {
fclose($pipe);
}
proc_close($process);
throw new RuntimeException('MX helper timed out');
}
usleep(100000);
} while (true);
$stdout .= stream_get_contents($pipes[1]) ?: '';
$stderr .= stream_get_contents($pipes[2]) ?: '';
fclose($pipes[1]);
fclose($pipes[2]);
$exit = proc_close($process);
return new ProcessResult($exit, substr($stdout, 0, 8192), substr($stderr, 0, 8192));
}
}