,int):ProcessResult */ private $runner; /** * @param null|callable(array,int):ProcessResult $runner */ public function __construct(?callable $runner = null) { $this->runner = $runner ?? [self::class, 'runProcess']; } /** * @return list */ 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, (string)$request->createdAt, ]; } 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 $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)); } }