From b23e9594911f327fbccf9569ead3db7ceff5b3eb Mon Sep 17 00:00:00 2001 From: Marek Miklewicz Date: Tue, 30 Jun 2026 12:06:32 +0200 Subject: [PATCH] feat: add mx login mode selection --- exec/lib/RemoteLoginUrlClient.php | 21 +++++++++++++++++++-- exec/lib/Settings.php | 9 +++++++++ plugin-settings.conf | 7 ++++++- plugin.conf | 2 +- tests/package_contents_test.php | 3 +-- tests/remote_login_url_client_test.php | 25 ++++++++++++++++++++++++- tests/settings_test.php | 14 ++++++++++++++ 7 files changed, 74 insertions(+), 7 deletions(-) diff --git a/exec/lib/RemoteLoginUrlClient.php b/exec/lib/RemoteLoginUrlClient.php index 6811853..08017ca 100644 --- a/exec/lib/RemoteLoginUrlClient.php +++ b/exec/lib/RemoteLoginUrlClient.php @@ -44,7 +44,7 @@ final class RemoteLoginUrlClient */ public static function buildCommand(Settings $settings, LoginUrlRequest $request): array { - return [ + $base = [ 'ssh', '-o', 'BatchMode=yes', @@ -59,6 +59,23 @@ final class RemoteLoginUrlClient '-p', (string)$settings->sshPort(), $settings->sshUser() . '@' . $settings->sshHost(), + ]; + + if ($settings->mxLoginMode() === 1) { + $command = array_merge($base, [ + '/usr/bin/da', + 'login-url', + '--user=' . $request->username, + '--redirect-url=' . $request->redirectPath, + '--expiry=' . $request->ttl . 's', + ]); + if ($request->userIpBound) { + $command[] = '--ip=' . $request->clientIp; + } + return $command; + } + + return array_merge($base, [ 'mail-login-create-url', $request->username, $request->sourceHost, @@ -68,7 +85,7 @@ final class RemoteLoginUrlClient $request->clientIp, $request->redirectPath, (string)$request->createdAt, - ]; + ]); } public function create(Settings $settings, LoginUrlRequest $request): string diff --git a/exec/lib/Settings.php b/exec/lib/Settings.php index 51d4eb3..13c93f9 100644 --- a/exec/lib/Settings.php +++ b/exec/lib/Settings.php @@ -58,6 +58,7 @@ final class Settings return [ 'DEFAULT_ENABLE' => 'true', 'MAIL_LOGIN_METHOD' => 'login_url', + 'MX_LOGIN_MODE' => '1', 'MAIL_LOGIN_TARGET_NAME' => 'mx1', 'MAIL_LOGIN_TARGET_BASE_URL' => 'https://mx1.mojserwer.pl:2222', 'MAIL_LOGIN_REDIRECT_URL' => '/', @@ -115,6 +116,11 @@ final class Settings return $this->getString('MAIL_LOGIN_METHOD', 'login_url'); } + public function mxLoginMode(): int + { + return (int)$this->getString('MX_LOGIN_MODE', '1'); + } + public function redirectUrl(): string { return $this->getString('MAIL_LOGIN_REDIRECT_URL', '/'); @@ -275,6 +281,9 @@ final class Settings if ($this->method() !== 'login_url') { throw new InvalidArgumentException('MAIL_LOGIN_METHOD must be login_url'); } + if (!in_array($this->getString('MX_LOGIN_MODE', '1'), ['1', '2'], true)) { + throw new InvalidArgumentException('MX_LOGIN_MODE must be 1 or 2'); + } if (!in_array($this->clientIpMode(), ['direct', 'trusted_proxy'], true)) { throw new InvalidArgumentException('MAIL_LOGIN_CLIENT_IP_MODE must be direct or trusted_proxy'); } diff --git a/plugin-settings.conf b/plugin-settings.conf index 2ef726e..a7aaa8d 100644 --- a/plugin-settings.conf +++ b/plugin-settings.conf @@ -4,6 +4,11 @@ DEFAULT_ENABLE=true # Metoda produkcyjna. Wersja 1 używa jednorazowego DirectAdmin login URL generowanego na MX. MAIL_LOGIN_METHOD=login_url +# Tryb tworzenia login URL na MX: +# 1 = direct SSH command: serwer źródłowy łączy się po SSH do MX i uruchamia bezpośrednio /usr/bin/da login-url; nie wymaga instalowania helpera na MX. +# 2 = helper: serwer źródłowy łączy się po SSH do ograniczonego helpera na MX; lepsza walidacja/audyt/cleanup, ale wymaga instalacji helpera. +MX_LOGIN_MODE=1 + # Nazwa i adres docelowego panelu poczty. MAIL_LOGIN_TARGET_NAME=mx1 MAIL_LOGIN_TARGET_BASE_URL=https://mx1.mojserwer.pl:2222 @@ -25,7 +30,7 @@ MAIL_LOGIN_SOURCE_SERVER_NAME= MAIL_LOGIN_CLIENT_IP_MODE=direct MAIL_LOGIN_TRUSTED_PROXIES= -# Serwerowy kanał SSH do helpera na MX. Klucz jest root-owned i nigdy nie trafia do użytkownika. +# Serwerowy kanał SSH do MX. Klucz jest root-owned i nigdy nie trafia do użytkownika. MAIL_LOGIN_REMOTE_TRANSPORT=ssh MAIL_LOGIN_SSH_HOST=mx1.mojserwer.pl MAIL_LOGIN_SSH_PORT=22 diff --git a/plugin.conf b/plugin.conf index 00d69b9..c458aa3 100644 --- a/plugin.conf +++ b/plugin.conf @@ -2,7 +2,7 @@ name=mail-login id=mail-login type=user author=HITME.PL -version=1.0.1 +version=1.0.2 active=no installed=no user_run_as=root diff --git a/tests/package_contents_test.php b/tests/package_contents_test.php index 772a2ef..624bece 100644 --- a/tests/package_contents_test.php +++ b/tests/package_contents_test.php @@ -8,7 +8,7 @@ test('plugin metadata and packaging docs match project rules', function (): void assert_contains('name=mail-login', $conf); assert_contains('id=mail-login', $conf); assert_contains('type=user', $conf); - assert_contains('version=1.0.1', $conf); + assert_contains('version=1.0.2', $conf); assert_contains('user_run_as=root', $conf); assert_contains('/Users/marek/GPT/ChatGPT/da_plugins/mail-login/src', $packing); assert_contains('/Users/marek/GPT/ChatGPT/da_plugins/mail-login/archives/X.Y.Z/mail-login.tar.gz', $packing); @@ -37,4 +37,3 @@ test('package source excludes local-only files and dangerous placeholders', func } assert_same([], $bad); }); - diff --git a/tests/remote_login_url_client_test.php b/tests/remote_login_url_client_test.php index 6540c97..b8df9ed 100644 --- a/tests/remote_login_url_client_test.php +++ b/tests/remote_login_url_client_test.php @@ -4,7 +4,7 @@ declare(strict_types=1); require_once PLUGIN_ROOT . '/exec/lib/Settings.php'; require_once PLUGIN_ROOT . '/exec/lib/RemoteLoginUrlClient.php'; -test('remote login url client builds fixed ssh command with positional arguments', function (): void { +test('remote login url client builds direct ssh command by default', function (): void { $settings = Settings::fromArray([ 'MAIL_LOGIN_TARGET_BASE_URL' => 'https://mx1.domena.pl:2222', 'MAIL_LOGIN_SSH_HOST' => 'mx1.domena.pl', @@ -21,6 +21,29 @@ test('remote login url client builds fixed ssh command with positional arguments assert_same('ssh', $command[0]); assert_contains('UserKnownHostsFile=/secure/known_hosts', implode(' ', $command)); assert_contains('root@mx1.domena.pl', implode(' ', $command)); + assert_not_contains('mail-login-create-url', implode(' ', $command)); + assert_contains('/usr/bin/da login-url', implode(' ', $command)); + assert_contains('--user=mxtest', implode(' ', $command)); + assert_contains('--expiry=30s', implode(' ', $command)); + assert_contains('--ip=198.51.100.10', implode(' ', $command)); +}); + +test('remote login url client builds helper ssh command when mode is helper', function (): void { + $settings = Settings::fromArray([ + 'MAIL_LOGIN_TARGET_BASE_URL' => 'https://mx1.domena.pl:2222', + 'MAIL_LOGIN_SSH_HOST' => 'mx1.domena.pl', + 'MAIL_LOGIN_SSH_PORT' => '222', + 'MAIL_LOGIN_SSH_USER' => 'root', + 'MAIL_LOGIN_SSH_IDENTITY' => '/secure/key', + 'MAIL_LOGIN_SSH_KNOWN_HOSTS' => '/secure/known_hosts', + 'LOGIN_KEY_TTL' => '30', + 'USER_IP_BOUND' => '1', + 'MX_LOGIN_MODE' => '2', + ]); + $request = new LoginUrlRequest('mxtest', 'h4.domena.pl', 'h4', 30, true, '198.51.100.10', '/', 1782810000); + $command = RemoteLoginUrlClient::buildCommand($settings, $request); + + assert_same('ssh', $command[0]); $pos = array_search('mail-login-create-url', $command, true); assert_true(is_int($pos), 'Command marker missing'); assert_same('mxtest', $command[$pos + 1]); diff --git a/tests/settings_test.php b/tests/settings_test.php index e5999be..25a1823 100644 --- a/tests/settings_test.php +++ b/tests/settings_test.php @@ -10,6 +10,7 @@ test('settings expose secure defaults from plugin-settings.conf', function (): v assert_true($settings->userIpBound()); assert_true($settings->defaultEnable()); assert_same('login_url', $settings->method()); + assert_same(1, $settings->mxLoginMode()); assert_same('root', $settings->sshUser()); assert_same('https://mx1.mojserwer.pl:2222', $settings->targetBaseUrl()); }); @@ -29,10 +30,23 @@ test('settings accept explicit ttl and user ip binding toggle', function (): voi $settings = Settings::fromArray([ 'LOGIN_KEY_TTL' => '5', 'USER_IP_BOUND' => '0', + 'MX_LOGIN_MODE' => '2', ]); assert_same(5, $settings->loginKeyTtl()); assert_false($settings->userIpBound()); + assert_same(2, $settings->mxLoginMode()); +}); + +test('settings reject unsupported mx login mode values', function (): void { + foreach (['0', '3', 'helper', ''] as $mode) { + try { + Settings::fromArray(['MX_LOGIN_MODE' => $mode]); + throw new RuntimeException('Expected invalid MX_LOGIN_MODE to fail: ' . $mode); + } catch (InvalidArgumentException $e) { + assert_contains('MX_LOGIN_MODE', $e->getMessage()); + } + } }); test('settings parse allowed source hosts as normalized hostnames', function (): void {