Compare commits
11 Commits
7d3d458160
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 631ab96736 | |||
| 311190a7cd | |||
| b6a9180eae | |||
| 79d7721733 | |||
| 7ec8f03248 | |||
| cec0b9ef9d | |||
| 8861611a67 | |||
| bb6a01fdca | |||
| 4a1c9d0db4 | |||
| e2814fd81c | |||
| d5894a1935 |
@@ -10,15 +10,12 @@ return static function (): void {
|
||||
if (!$settings->defaultEnable()) {
|
||||
Http::errorPage('Plugin nie jest włączony dla tego serwera.', 403);
|
||||
}
|
||||
if ($settings->method() !== 'login_url') {
|
||||
Http::errorPage('Metoda logowania nie jest obsługiwana.', 500);
|
||||
}
|
||||
$ctx = DirectAdminContext::fromServer($_SERVER, $settings);
|
||||
|
||||
$auditBase = [
|
||||
'request_id' => bin2hex(random_bytes(8)),
|
||||
'source_host' => $ctx->sourceHost(),
|
||||
'server_name' => $ctx->serverName(),
|
||||
'server_name' => $ctx->auditServerName(),
|
||||
'target_host' => $settings->targetBaseUrl(),
|
||||
'username' => $ctx->username(),
|
||||
'ttl' => $settings->loginKeyTtl(),
|
||||
@@ -53,7 +50,7 @@ return static function (): void {
|
||||
'event' => 'create_login_url',
|
||||
'result' => 'success',
|
||||
]);
|
||||
Http::redirectNoStore($url);
|
||||
Http::browserRedirectNoStore($url);
|
||||
} catch (Throwable $e) {
|
||||
AuditLog::append(Settings::AUDIT_LOG, $auditBase + [
|
||||
'event' => 'create_login_url',
|
||||
|
||||
@@ -57,6 +57,11 @@ final class DirectAdminContext
|
||||
return $this->serverName;
|
||||
}
|
||||
|
||||
public function auditServerName(): string
|
||||
{
|
||||
return $this->sourceHost;
|
||||
}
|
||||
|
||||
public function method(): string
|
||||
{
|
||||
return $this->method;
|
||||
|
||||
+98
-14
@@ -37,26 +37,110 @@ final class Http
|
||||
|
||||
public static function redirectNoStore(string $url): void
|
||||
{
|
||||
header('Cache-Control: no-store, private');
|
||||
header('Pragma: no-cache');
|
||||
header('Referrer-Policy: no-referrer');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Location: ' . $url, true, 302);
|
||||
self::sendResponse(302, [
|
||||
'Cache-Control' => 'no-store, private',
|
||||
'Pragma' => 'no-cache',
|
||||
'Referrer-Policy' => 'no-referrer',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
'Location' => $url,
|
||||
], '');
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function browserRedirectNoStore(string $url): void
|
||||
{
|
||||
self::sendResponse(200, [
|
||||
'Content-Type' => 'text/html; charset=UTF-8',
|
||||
'Cache-Control' => 'no-store, private',
|
||||
'Pragma' => 'no-cache',
|
||||
'Referrer-Policy' => 'no-referrer',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
'Content-Security-Policy' => "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'",
|
||||
], self::redirectPageHtml($url));
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function redirectPageHtml(string $url): string
|
||||
{
|
||||
$safeUrl = htmlspecialchars($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
$jsonUrl = json_encode($url, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
|
||||
if (!is_string($jsonUrl)) {
|
||||
$jsonUrl = '"/"';
|
||||
}
|
||||
|
||||
return '<!doctype html><html lang="pl"><head><meta charset="utf-8">'
|
||||
. '<meta name="viewport" content="width=device-width, initial-scale=1">'
|
||||
. '<meta http-equiv="refresh" content="0;url=' . $safeUrl . '">'
|
||||
. '<title>Zaloguj do serwera poczty</title></head><body>'
|
||||
. '<script>(function(){var target=' . $jsonUrl . ';'
|
||||
. 'try{if(window.top){window.top.location.replace(target);return;}}catch(e){}'
|
||||
. 'window.location.replace(target);}());</script>'
|
||||
. '<p><a rel="noreferrer" href="' . $safeUrl . '">Otwórz panel poczty</a></p>'
|
||||
. '</body></html>';
|
||||
}
|
||||
|
||||
public static function errorPage(string $message, int $status): void
|
||||
{
|
||||
http_response_code($status);
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
header('Cache-Control: no-store, private');
|
||||
header('Referrer-Policy: no-referrer');
|
||||
$safe = htmlspecialchars($message, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
echo '<!doctype html><html lang="pl"><head><meta charset="utf-8">';
|
||||
echo '<meta name="viewport" content="width=device-width, initial-scale=1">';
|
||||
echo '<title>Serwer poczty</title></head><body>';
|
||||
echo '<h1>Serwer poczty</h1><p>' . $safe . '</p>';
|
||||
echo '</body></html>';
|
||||
$body = '<!doctype html><html lang="pl"><head><meta charset="utf-8">'
|
||||
. '<meta name="viewport" content="width=device-width, initial-scale=1">'
|
||||
. '<title>Zaloguj do serwera poczty</title></head><body>'
|
||||
. '<h1>Zaloguj do serwera poczty</h1><p>' . $safe . '</p>'
|
||||
. '</body></html>';
|
||||
self::sendResponse($status, [
|
||||
'Content-Type' => 'text/html; charset=UTF-8',
|
||||
'Cache-Control' => 'no-store, private',
|
||||
'Referrer-Policy' => 'no-referrer',
|
||||
], $body);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string> $headers
|
||||
*/
|
||||
public static function rawHttpResponse(int $status, array $headers, string $body): string
|
||||
{
|
||||
$headers['Content-Length'] = (string)strlen($body);
|
||||
$lines = ['HTTP/1.1 ' . $status . ' ' . self::reasonPhrase($status)];
|
||||
foreach ($headers as $name => $value) {
|
||||
$safeName = preg_replace('/[^A-Za-z0-9-]/', '', (string)$name) ?? '';
|
||||
if ($safeName === '') {
|
||||
continue;
|
||||
}
|
||||
$safeValue = str_replace(["\r", "\n"], '', (string)$value);
|
||||
$lines[] = $safeName . ': ' . $safeValue;
|
||||
}
|
||||
return implode("\r\n", $lines) . "\r\n\r\n" . $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string> $headers
|
||||
*/
|
||||
private static function sendResponse(int $status, array $headers, string $body): void
|
||||
{
|
||||
if (PHP_SAPI === 'cli') {
|
||||
echo self::rawHttpResponse($status, $headers, $body);
|
||||
return;
|
||||
}
|
||||
|
||||
http_response_code($status);
|
||||
foreach ($headers as $name => $value) {
|
||||
header($name . ': ' . $value);
|
||||
}
|
||||
header('Content-Length: ' . strlen($body));
|
||||
echo $body;
|
||||
}
|
||||
|
||||
private static function reasonPhrase(int $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
200 => 'OK',
|
||||
302 => 'Found',
|
||||
403 => 'Forbidden',
|
||||
405 => 'Method Not Allowed',
|
||||
500 => 'Internal Server Error',
|
||||
502 => 'Bad Gateway',
|
||||
default => 'OK',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,31 +61,19 @@ final class RemoteLoginUrlClient
|
||||
$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,
|
||||
$request->serverName,
|
||||
(string)$request->ttl,
|
||||
$request->userIpBound ? '1' : '0',
|
||||
$request->clientIp,
|
||||
$request->redirectPath,
|
||||
(string)$request->createdAt,
|
||||
$command = array_merge($base, [
|
||||
'/usr/bin/da',
|
||||
'login-url',
|
||||
'--user=' . $request->username,
|
||||
'--redirect-url=' . $request->redirectPath,
|
||||
]);
|
||||
if ($request->ttl > 0) {
|
||||
$command[] = '--expiry=' . $request->ttl . 's';
|
||||
}
|
||||
if ($request->userIpBound) {
|
||||
$command[] = '--ip=' . $request->clientIp;
|
||||
}
|
||||
return $command;
|
||||
}
|
||||
|
||||
public function create(Settings $settings, LoginUrlRequest $request): string
|
||||
@@ -99,7 +87,7 @@ final class RemoteLoginUrlClient
|
||||
throw new RuntimeException(self::failureMessage($settings, $result));
|
||||
}
|
||||
if (!preg_match('/https:\/\/\S+/', $result->stdout, $match)) {
|
||||
throw new RuntimeException('MX helper did not return a login URL');
|
||||
throw new RuntimeException('MX direct SSH command did not return a login URL');
|
||||
}
|
||||
try {
|
||||
return UrlValidator::assertGeneratedLoginUrl(trim($match[0]), $settings);
|
||||
@@ -110,8 +98,7 @@ final class RemoteLoginUrlClient
|
||||
|
||||
private static function failureMessage(Settings $settings, ProcessResult $result): string
|
||||
{
|
||||
$message = $settings->mxLoginMode() === 1 ? 'MX direct SSH command failed' : 'MX helper failed';
|
||||
$message .= ' (exit ' . $result->exitCode . ')';
|
||||
$message = 'MX direct SSH command failed (exit ' . $result->exitCode . ')';
|
||||
$detail = self::safeDiagnostic($result->stderr);
|
||||
if ($detail !== '') {
|
||||
$message .= ': ' . $detail;
|
||||
@@ -138,7 +125,7 @@ final class RemoteLoginUrlClient
|
||||
];
|
||||
$process = proc_open($command, $descriptors, $pipes);
|
||||
if (!is_resource($process)) {
|
||||
throw new RuntimeException('Cannot start MX helper');
|
||||
throw new RuntimeException('Cannot start MX direct SSH command');
|
||||
}
|
||||
fclose($pipes[0]);
|
||||
stream_set_blocking($pipes[1], false);
|
||||
@@ -159,7 +146,7 @@ final class RemoteLoginUrlClient
|
||||
fclose($pipe);
|
||||
}
|
||||
proc_close($process);
|
||||
throw new RuntimeException('MX helper timed out');
|
||||
throw new RuntimeException('MX direct SSH command timed out');
|
||||
}
|
||||
usleep(100000);
|
||||
} while (true);
|
||||
|
||||
+6
-25
@@ -3,8 +3,8 @@ declare(strict_types=1);
|
||||
|
||||
final class Settings
|
||||
{
|
||||
public const BASE_DIR = '/usr/local/hitme_plugins/mail-login';
|
||||
public const PLUGIN_DIR = '/usr/local/directadmin/plugins/mail-login';
|
||||
public const BASE_DIR = '/usr/local/hitme_plugins/mxautologin';
|
||||
public const PLUGIN_DIR = '/usr/local/directadmin/plugins/mxautologin';
|
||||
public const SECRET_DIR = self::BASE_DIR . '/secrets';
|
||||
public const LOG_DIR = self::BASE_DIR . '/logs';
|
||||
public const STATE_DIR = self::BASE_DIR . '/state';
|
||||
@@ -57,12 +57,9 @@ 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' => '/',
|
||||
'LOGIN_KEY_TTL' => '30',
|
||||
'LOGIN_KEY_TTL' => '14400',
|
||||
'USER_IP_BOUND' => '1',
|
||||
'IP_MASK' => '1',
|
||||
'MAIL_LOGIN_SOURCE_SERVER_NAME' => '',
|
||||
@@ -111,16 +108,6 @@ final class Settings
|
||||
return in_array(strtolower($this->getString('DEFAULT_ENABLE', 'true')), ['1', 'true', 'yes', 'on'], true);
|
||||
}
|
||||
|
||||
public function method(): string
|
||||
{
|
||||
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', '/');
|
||||
@@ -128,7 +115,7 @@ final class Settings
|
||||
|
||||
public function loginKeyTtl(): int
|
||||
{
|
||||
return (int)$this->getString('LOGIN_KEY_TTL', '30');
|
||||
return (int)$this->getString('LOGIN_KEY_TTL', '14400');
|
||||
}
|
||||
|
||||
public function userIpBound(): bool
|
||||
@@ -252,8 +239,8 @@ final class Settings
|
||||
throw new InvalidArgumentException('LOGIN_KEY_TTL must be an integer');
|
||||
}
|
||||
$ttl = (int)$ttlRaw;
|
||||
if ($ttl < 5 || $ttl > 300) {
|
||||
throw new InvalidArgumentException('LOGIN_KEY_TTL must be between 5 and 300 seconds');
|
||||
if ($ttl !== 0 && ($ttl < 5 || $ttl > 86400)) {
|
||||
throw new InvalidArgumentException('LOGIN_KEY_TTL must be 0 or between 5 and 86400 seconds');
|
||||
}
|
||||
if (!in_array($this->getString('USER_IP_BOUND', ''), ['0', '1'], true)) {
|
||||
throw new InvalidArgumentException('USER_IP_BOUND must be 0 or 1');
|
||||
@@ -264,12 +251,6 @@ final class Settings
|
||||
if (!in_array(strtolower($this->getString('DEFAULT_ENABLE', 'true')), ['0', '1', 'true', 'false', 'yes', 'no', 'on', 'off'], true)) {
|
||||
throw new InvalidArgumentException('DEFAULT_ENABLE must be a boolean value');
|
||||
}
|
||||
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');
|
||||
}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
<a href="/CMD_PLUGINS/mail-login/login.raw" target="_top"><img src="/CMD_PLUGINS/mail-login/images/user_icon.svg" width="16" height="16" alt="Serwer poczty"/></a>
|
||||
<a href="/CMD_PLUGINS/mxautologin/login.raw" target="_top"><img src="/CMD_PLUGINS/mxautologin/images/user_icon.svg" width="16" height="16" alt="Zaloguj do serwera poczty"/></a>
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
<a href="/CMD_PLUGINS/mail-login/login.raw" target="_top">Serwer poczty</a>
|
||||
<a href="/CMD_PLUGINS/mxautologin/login.raw" target="_top">Zaloguj do serwera poczty</a>
|
||||
|
||||
+13
-4
@@ -1,5 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" role="img" aria-label="Serwer poczty">
|
||||
<rect x="2" y="5" width="20" height="14" rx="2" fill="#14532d"/>
|
||||
<path d="M4 7l8 6 8-6" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7 16h10" fill="none" stroke="#bbf7d0" stroke-width="2" stroke-linecap="round"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" role="img" aria-label="Zaloguj do serwera poczty">
|
||||
<defs>
|
||||
<linearGradient id="mxLoginGradient" x1="2" y1="2" x2="22" y2="22" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#6670d6"/>
|
||||
<stop offset=".52" stop-color="#4f96c8"/>
|
||||
<stop offset="1" stop-color="#2ec4b6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="2" y="2" width="20" height="20" rx="5" fill="url(#mxLoginGradient)"/>
|
||||
<path d="M5 8.25a2 2 0 0 1 2-2h9.25a2 2 0 0 1 2 2v7.5a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2z" fill="#ff8a00" stroke="#2f95c8" stroke-width=".35"/>
|
||||
<path d="M6.4 8.85 11.65 12.35 16.9 8.85" fill="none" stroke="#ffffff" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.25 15.55h7.4" fill="none" stroke="#ffffff" stroke-width="1.35" stroke-linecap="round" opacity=".75"/>
|
||||
<path d="M14.75 12h4.8m-1.7-1.7 1.7 1.7-1.7 1.7" fill="none" stroke="#ffffff" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 408 B After Width: | Height: | Size: 1.0 KiB |
@@ -1,160 +0,0 @@
|
||||
#!/usr/local/bin/php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
if (realpath((string)($_SERVER['SCRIPT_FILENAME'] ?? '')) === __FILE__) {
|
||||
mailLoginCleanupMain();
|
||||
}
|
||||
|
||||
function mailLoginCleanupMain(): void
|
||||
{
|
||||
$config = [
|
||||
'base_url' => getenv('DA_BASE_URL') ?: 'https://127.0.0.1:2222',
|
||||
'user' => getenv('DA_API_USER') ?: '',
|
||||
'key' => getenv('DA_API_LOGIN_KEY') ?: '',
|
||||
'audit_log' => getenv('MAIL_LOGIN_AUDIT_LOG') ?: '/usr/local/hitme_plugins/mail-login-helper/logs/audit.log',
|
||||
'state_dir' => getenv('MAIL_LOGIN_STATE_DIR') ?: '/usr/local/hitme_plugins/mail-login-helper/state',
|
||||
];
|
||||
|
||||
if ($config['user'] === '' || $config['key'] === '') {
|
||||
fwrite(STDERR, "DirectAdmin API credentials are required for cleanup\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
$base = mailLoginCleanupValidateBaseUrl($config['base_url']);
|
||||
$now = time();
|
||||
$states = mailLoginCleanupLoadStates($config['state_dir']);
|
||||
$entries = mailLoginCleanupApiRequest('GET', $base . '/api/login-keys/urls', $config);
|
||||
if (!is_array($entries)) {
|
||||
fwrite(STDERR, "DirectAdmin returned invalid login URL list\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$deleted = 0;
|
||||
foreach ($entries as $entry) {
|
||||
if (is_array($entry) && mailLoginCleanupShouldDelete($entry, $states, $now)) {
|
||||
mailLoginCleanupApiRequest('DELETE', $base . '/api/login-keys/urls/' . rawurlencode((string)$entry['id']), $config);
|
||||
$deleted++;
|
||||
}
|
||||
}
|
||||
|
||||
mailLoginCleanupAudit($config['audit_log'], [
|
||||
'event' => 'cleanup_expired_login_urls',
|
||||
'result' => 'success',
|
||||
'deleted' => $deleted,
|
||||
]);
|
||||
|
||||
echo "Deleted expired login URLs: {$deleted}\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string,mixed>>
|
||||
*/
|
||||
function mailLoginCleanupLoadStates(string $stateDir): array
|
||||
{
|
||||
$states = [];
|
||||
foreach (glob(rtrim($stateDir, '/') . '/autologin-*.json') ?: [] as $file) {
|
||||
$decoded = json_decode((string)file_get_contents($file), true);
|
||||
if (is_array($decoded)) {
|
||||
$states[] = $decoded;
|
||||
}
|
||||
}
|
||||
return $states;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $entry
|
||||
* @param list<array<string,mixed>> $states
|
||||
*/
|
||||
function mailLoginCleanupShouldDelete(array $entry, array $states, int $now): bool
|
||||
{
|
||||
if (empty($entry['id']) || empty($entry['expires'])) {
|
||||
return false;
|
||||
}
|
||||
$entryExpires = strtotime((string)$entry['expires']);
|
||||
if ($entryExpires === false || $entryExpires >= $now) {
|
||||
return false;
|
||||
}
|
||||
foreach ($states as $state) {
|
||||
if ((string)($state['id'] ?? '') === '' || (string)$entry['id'] !== (string)$state['id']) {
|
||||
continue;
|
||||
}
|
||||
if (($state['status'] ?? '') !== 'created') {
|
||||
continue;
|
||||
}
|
||||
$stateExpires = (int)($state['expires'] ?? 0);
|
||||
if ($stateExpires <= 0 || $stateExpires > $now) {
|
||||
continue;
|
||||
}
|
||||
if ((string)($entry['redirectURL'] ?? '/') !== (string)($state['redirect_path'] ?? '/')) {
|
||||
continue;
|
||||
}
|
||||
$entryNetworks = $entry['allowNetworks'] ?? [];
|
||||
if (!is_array($entryNetworks)) {
|
||||
$entryNetworks = [];
|
||||
}
|
||||
$stateIp = (string)($state['client_ip'] ?? '');
|
||||
$ipBound = (int)($state['ip_bound'] ?? 0) === 1;
|
||||
if ($ipBound && !in_array($stateIp . (str_contains($stateIp, ':') ? '/128' : '/32'), $entryNetworks, true)) {
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function mailLoginCleanupValidateBaseUrl(string $baseUrl): string
|
||||
{
|
||||
$baseUrl = rtrim($baseUrl, '/');
|
||||
$parts = parse_url($baseUrl);
|
||||
if (!is_array($parts) || strtolower((string)($parts['scheme'] ?? '')) !== 'https' || empty($parts['host'])) {
|
||||
throw new InvalidArgumentException('DA_BASE_URL must be an https URL');
|
||||
}
|
||||
return $baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string> $config
|
||||
* @return mixed
|
||||
*/
|
||||
function mailLoginCleanupApiRequest(string $method, string $url, array $config): mixed
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
if ($ch === false) {
|
||||
throw new RuntimeException('Cannot initialize curl');
|
||||
}
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_CUSTOMREQUEST => $method,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
|
||||
CURLOPT_USERPWD => $config['user'] . ':' . $config['key'],
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_TIMEOUT => 20,
|
||||
CURLOPT_HTTPHEADER => ['Accept: application/json'],
|
||||
]);
|
||||
$body = curl_exec($ch);
|
||||
$code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
if ($body === false || $code >= 400) {
|
||||
throw new RuntimeException($error !== '' ? $error : 'DirectAdmin API request failed');
|
||||
}
|
||||
if ($method === 'DELETE') {
|
||||
return true;
|
||||
}
|
||||
return json_decode((string)$body, true, 512, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $fields
|
||||
*/
|
||||
function mailLoginCleanupAudit(string $path, array $fields): void
|
||||
{
|
||||
$dir = dirname($path);
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0700, true);
|
||||
}
|
||||
$fields['timestamp'] = gmdate('c');
|
||||
file_put_contents($path, json_encode($fields, JSON_UNESCAPED_SLASHES) . "\n", FILE_APPEND | LOCK_EX);
|
||||
chmod($path, 0600);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
BASE_DIR="/usr/local/hitme_plugins/mail-login-helper"
|
||||
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||
|
||||
mkdir -p "$BASE_DIR/bin" "$BASE_DIR/config" "$BASE_DIR/logs" "$BASE_DIR/state"
|
||||
chmod 700 "$BASE_DIR" "$BASE_DIR/bin" "$BASE_DIR/config" "$BASE_DIR/logs" "$BASE_DIR/state"
|
||||
|
||||
cp "$SCRIPT_DIR/mail-login-authorized-command.sh" "$BASE_DIR/bin/create-login-url"
|
||||
cp "$SCRIPT_DIR/cleanup-expired-login-urls.php" "$BASE_DIR/bin/cleanup-expired-login-urls"
|
||||
chmod 755 "$BASE_DIR/bin/create-login-url" "$BASE_DIR/bin/cleanup-expired-login-urls"
|
||||
touch "$BASE_DIR/logs/audit.log"
|
||||
chmod 600 "$BASE_DIR/logs/audit.log"
|
||||
|
||||
if [ ! -f "$BASE_DIR/config/helper-settings.conf" ]; then
|
||||
cat > "$BASE_DIR/config/helper-settings.conf" <<'EOF'
|
||||
MAIL_LOGIN_ALLOWED_SOURCE_HOSTS=h4.domena.pl
|
||||
MAIL_LOGIN_DA_BIN=/usr/bin/da
|
||||
EOF
|
||||
chmod 600 "$BASE_DIR/config/helper-settings.conf"
|
||||
fi
|
||||
|
||||
cat > "$BASE_DIR/authorized_keys.example" <<'EOF'
|
||||
from="SOURCE_SERVER_PUBLIC_IP",restrict,no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-pty,command="/usr/local/hitme_plugins/mail-login-helper/bin/create-login-url" ssh-ed25519 SOURCE_SERVER_PUBLIC_KEY mail-login-source
|
||||
EOF
|
||||
chmod 600 "$BASE_DIR/authorized_keys.example"
|
||||
|
||||
echo "mail-login MX helper installed at $BASE_DIR"
|
||||
@@ -1,127 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
set -f
|
||||
|
||||
DA_BIN="${MAIL_LOGIN_DA_BIN:-/usr/bin/da}"
|
||||
CONFIG_FILE="${MAIL_LOGIN_HELPER_CONFIG:-/usr/local/hitme_plugins/mail-login-helper/config/helper-settings.conf}"
|
||||
AUDIT_LOG="${MAIL_LOGIN_AUDIT_LOG:-/usr/local/hitme_plugins/mail-login-helper/logs/audit.log}"
|
||||
STATE_DIR="${MAIL_LOGIN_STATE_DIR:-/usr/local/hitme_plugins/mail-login-helper/state}"
|
||||
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
. "$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
audit() {
|
||||
dir=$(dirname "$AUDIT_LOG")
|
||||
mkdir -p "$dir"
|
||||
chmod 700 "$dir"
|
||||
php -r '$fields=["timestamp"=>gmdate("c"),"event"=>$argv[1],"result"=>$argv[2],"correlation"=>$argv[3],"username"=>$argv[4],"source_host"=>$argv[5],"client_ip"=>$argv[6]]; echo json_encode($fields, JSON_UNESCAPED_SLASHES)."\n";' \
|
||||
"$1" "$2" "$3" "$4" "$5" "$6" >> "$AUDIT_LOG"
|
||||
chmod 600 "$AUDIT_LOG"
|
||||
}
|
||||
|
||||
fail() {
|
||||
audit "create_login_url" "failure" "${CORRELATION:-unknown}" "${USERNAME_ARG:-unknown}" "${SOURCE_HOST:-unknown}" "${CLIENT_IP:-unknown}"
|
||||
echo "mail-login helper failed" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
clean_label() {
|
||||
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g; s/^-*//; s/-*$//'
|
||||
}
|
||||
|
||||
first_label() {
|
||||
printf '%s' "$1" | sed 's/:.*$//' | awk -F. '{print $1}'
|
||||
}
|
||||
|
||||
host_allowed() {
|
||||
allowed="${MAIL_LOGIN_ALLOWED_SOURCE_HOSTS:-}"
|
||||
[ -n "$allowed" ] || return 1
|
||||
old_ifs=$IFS
|
||||
IFS=,
|
||||
for host in $allowed; do
|
||||
normalized=$(printf '%s' "$host" | tr '[:upper:]' '[:lower:]' | sed 's#^https\?://##; s#/.*$##; s/:[0-9][0-9]*$//; s/^ *//; s/ *$//')
|
||||
[ "$normalized" = "$SOURCE_HOST" ] && IFS=$old_ifs && return 0
|
||||
done
|
||||
IFS=$old_ifs
|
||||
return 1
|
||||
}
|
||||
|
||||
valid_ip() {
|
||||
php -r 'exit(filter_var($argv[1], FILTER_VALIDATE_IP) ? 0 : 1);' "$1"
|
||||
}
|
||||
|
||||
if [ "$#" -eq 0 ] && [ -n "${SSH_ORIGINAL_COMMAND:-}" ]; then
|
||||
# The authorized_keys forced command receives the requested command here.
|
||||
# All accepted fields are validated below before use.
|
||||
set -- $SSH_ORIGINAL_COMMAND
|
||||
fi
|
||||
|
||||
COMMAND="${1:-}"
|
||||
USERNAME_ARG="${2:-}"
|
||||
SOURCE_HOST="${3:-}"
|
||||
SERVER_NAME="${4:-}"
|
||||
TTL="${5:-}"
|
||||
USER_IP_BOUND="${6:-}"
|
||||
CLIENT_IP="${7:-}"
|
||||
REDIRECT_PATH="${8:-}"
|
||||
TIMESTAMP_ARG="${9:-}"
|
||||
|
||||
[ "$COMMAND" = "mail-login-create-url" ] || fail
|
||||
printf '%s' "$USERNAME_ARG" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$' || fail
|
||||
printf '%s' "$SOURCE_HOST" | grep -Eq '^[A-Za-z0-9.-]+$' || fail
|
||||
SOURCE_HOST=$(printf '%s' "$SOURCE_HOST" | tr '[:upper:]' '[:lower:]')
|
||||
host_allowed || fail
|
||||
SERVER_NAME=$(first_label "$SOURCE_HOST")
|
||||
SERVER_NAME=$(clean_label "$SERVER_NAME")
|
||||
printf '%s' "$SERVER_NAME" | grep -Eq '^[a-z0-9-]{1,32}$' || fail
|
||||
printf '%s' "$TTL" | grep -Eq '^[0-9]+$' || fail
|
||||
[ "$TTL" -ge 5 ] && [ "$TTL" -le 300 ] || fail
|
||||
[ "$USER_IP_BOUND" = "0" ] || [ "$USER_IP_BOUND" = "1" ] || fail
|
||||
valid_ip "$CLIENT_IP" || fail
|
||||
case "$REDIRECT_PATH" in
|
||||
/*) ;;
|
||||
*) fail ;;
|
||||
esac
|
||||
case "$REDIRECT_PATH" in
|
||||
//*|*\\*) fail ;;
|
||||
esac
|
||||
printf '%s' "$REDIRECT_PATH" | grep -Eq '^/[A-Za-z0-9._~%/?=+-]*$' || fail
|
||||
printf '%s' "$TIMESTAMP_ARG" | grep -Eq '^[0-9]+$' || fail
|
||||
|
||||
SERVER_CLEAN=$(clean_label "$SERVER_NAME")
|
||||
USER_CLEAN=$(clean_label "$USERNAME_ARG")
|
||||
TIMESTAMP="$TIMESTAMP_ARG"
|
||||
CORRELATION="autologin-$SERVER_CLEAN-$USER_CLEAN-$TIMESTAMP"
|
||||
|
||||
if [ ${#CORRELATION} -gt 96 ]; then
|
||||
HASH=$(printf '%s' "$CORRELATION" | openssl dgst -sha256 -r 2>/dev/null | awk '{print substr($1,1,10)}')
|
||||
SUFFIX="-$HASH-$TIMESTAMP"
|
||||
BODY=$(printf '%s' "autologin-$SERVER_CLEAN-$USER_CLEAN" | cut -c 1-$((96 - ${#SUFFIX})) | sed 's/-*$//')
|
||||
CORRELATION="$BODY$SUFFIX"
|
||||
fi
|
||||
|
||||
mkdir -p "$STATE_DIR"
|
||||
chmod 700 "$STATE_DIR"
|
||||
|
||||
if [ "$USER_IP_BOUND" = "1" ]; then
|
||||
OUTPUT=$("$DA_BIN" login-url "--user=$USERNAME_ARG" "--redirect-url=$REDIRECT_PATH" "--expiry=${TTL}s" "--ip=$CLIENT_IP") || fail
|
||||
else
|
||||
OUTPUT=$("$DA_BIN" login-url "--user=$USERNAME_ARG" "--redirect-url=$REDIRECT_PATH" "--expiry=${TTL}s") || fail
|
||||
audit "ip_bound_disabled" "warning" "$CORRELATION" "$USERNAME_ARG" "$SOURCE_HOST" "$CLIENT_IP"
|
||||
fi
|
||||
|
||||
URL=$(printf '%s\n' "$OUTPUT" | awk '/https:\/\// {for (i=1;i<=NF;i++) if ($i ~ /^https:\/\//) {print $i; exit}}')
|
||||
[ -n "$URL" ] || fail
|
||||
case "$URL" in
|
||||
https://*/api/login/url?key=*) ;;
|
||||
*) fail ;;
|
||||
esac
|
||||
LOGIN_URL_ID=$(printf '%s\n' "$OUTPUT" | awk 'match($0,/HASHURL[A-Z0-9]+/) {print substr($0,RSTART,RLENGTH); exit}')
|
||||
|
||||
php -r '$fields=["timestamp"=>(int)$argv[1],"expires"=>(int)$argv[2],"correlation"=>$argv[3],"id"=>$argv[4],"username"=>$argv[5],"source_host"=>$argv[6],"client_ip"=>$argv[7],"ttl"=>(int)$argv[8],"ip_bound"=>(int)$argv[9],"redirect_path"=>$argv[10],"status"=>"created"]; echo json_encode($fields, JSON_UNESCAPED_SLASHES)."\n";' \
|
||||
"$TIMESTAMP" "$((TIMESTAMP + TTL))" "$CORRELATION" "$LOGIN_URL_ID" "$USERNAME_ARG" "$SOURCE_HOST" "$CLIENT_IP" "$TTL" "$USER_IP_BOUND" "$REDIRECT_PATH" > "$STATE_DIR/$CORRELATION.json"
|
||||
chmod 600 "$STATE_DIR/$CORRELATION.json"
|
||||
audit "create_login_url" "success" "$CORRELATION" "$USERNAME_ARG" "$SOURCE_HOST" "$CLIENT_IP"
|
||||
printf 'URL: %s\n' "$URL"
|
||||
+10
-14
@@ -1,25 +1,21 @@
|
||||
# Ten plik jest runtime configuration pluginu.
|
||||
# Plugin czyta ustawienia bezpośrednio z:
|
||||
# /usr/local/directadmin/plugins/mail-login/plugin-settings.conf
|
||||
# /usr/local/directadmin/plugins/mxautologin/plugin-settings.conf
|
||||
|
||||
# Czy plugin jest domyślnie dostępny dla wszystkich użytkowników.
|
||||
DEFAULT_ENABLE=true
|
||||
|
||||
# Metoda produkcyjna. Wersja 1 używa jednorazowego DirectAdmin login URL generowanego na MX.
|
||||
MAIL_LOGIN_METHOD=login_url
|
||||
# Login URL jest tworzony wyłącznie przez direct SSH command:
|
||||
# serwer źródłowy łączy się po SSH do MX i uruchamia bezpośrednio /usr/bin/da 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
|
||||
# Adres docelowego panelu poczty.
|
||||
MAIL_LOGIN_TARGET_BASE_URL=https://mx1.mojserwer.pl:2222
|
||||
MAIL_LOGIN_REDIRECT_URL=/
|
||||
|
||||
# Czas życia jednorazowego login URL w sekundach. Dozwolony zakres: 5-300.
|
||||
LOGIN_KEY_TTL=30
|
||||
# Czas życia jednorazowego login URL w sekundach.
|
||||
# 0 = nie przekazuj --expiry i użyj domyślnego DirectAdmin login_hash_expiry_minutes.
|
||||
# Dozwolony zakres: 0 albo 5-86400. Domyślnie 14400 sekund, czyli 4h.
|
||||
LOGIN_KEY_TTL=14400
|
||||
|
||||
# 1 = przypnij URL do IP przeglądarki użytkownika, 0 = nie przypinaj do IP.
|
||||
USER_IP_BOUND=1
|
||||
@@ -40,8 +36,8 @@ MAIL_LOGIN_REMOTE_TRANSPORT=ssh
|
||||
MAIL_LOGIN_SSH_HOST=mx1.mojserwer.pl
|
||||
MAIL_LOGIN_SSH_PORT=22
|
||||
MAIL_LOGIN_SSH_USER=root
|
||||
MAIL_LOGIN_SSH_IDENTITY=/usr/local/hitme_plugins/mail-login/secrets/mx1_ed25519
|
||||
MAIL_LOGIN_SSH_KNOWN_HOSTS=/usr/local/hitme_plugins/mail-login/secrets/known_hosts
|
||||
MAIL_LOGIN_SSH_IDENTITY=/usr/local/hitme_plugins/mxautologin/secrets/mx1_ed25519
|
||||
MAIL_LOGIN_SSH_KNOWN_HOSTS=/usr/local/hitme_plugins/mxautologin/secrets/known_hosts
|
||||
MAIL_LOGIN_CONNECT_TIMEOUT_SECONDS=5
|
||||
|
||||
# Limity bezpieczeństwa na źródłowym serwerze DirectAdmin.
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
name=mail-login
|
||||
id=mail-login
|
||||
name=MX-Autologin
|
||||
id=mxautologin
|
||||
type=user
|
||||
author=HITME.PL
|
||||
version=1.0.9
|
||||
version=1.1.6
|
||||
active=no
|
||||
installed=no
|
||||
user_run_as=root
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
BASE_DIR="/usr/local/hitme_plugins/mail-login"
|
||||
PLUGIN_DIR="/usr/local/directadmin/plugins/mail-login"
|
||||
BASE_DIR="/usr/local/hitme_plugins/mxautologin"
|
||||
PLUGIN_DIR="/usr/local/directadmin/plugins/mxautologin"
|
||||
|
||||
mkdir -p "$BASE_DIR/secrets" "$BASE_DIR/logs" "$BASE_DIR/state/rate-limit"
|
||||
chmod 700 "$BASE_DIR/secrets" "$BASE_DIR/logs" "$BASE_DIR/state" "$BASE_DIR/state/rate-limit"
|
||||
@@ -14,4 +14,4 @@ fi
|
||||
touch "$BASE_DIR/logs/audit.log"
|
||||
chmod 600 "$BASE_DIR/logs/audit.log"
|
||||
|
||||
echo "mail-login installed; configuration is read from $PLUGIN_DIR/plugin-settings.conf"
|
||||
echo "MX-Autologin installed; configuration is read from $PLUGIN_DIR/plugin-settings.conf"
|
||||
|
||||
+5
-20
@@ -1,12 +1,10 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
KEY_DIR="${MAIL_LOGIN_KEY_DIR:-/usr/local/hitme_plugins/mail-login/secrets}"
|
||||
KEY_DIR="${MAIL_LOGIN_KEY_DIR:-/usr/local/hitme_plugins/mxautologin/secrets}"
|
||||
KEY_NAME="${MAIL_LOGIN_KEY_NAME:-mx1_ed25519}"
|
||||
KEY_PATH="$KEY_DIR/$KEY_NAME"
|
||||
MX_MODE="${MX_LOGIN_MODE:-1}"
|
||||
SETTINGS_FILE="${MAIL_LOGIN_SETTINGS_FILE:-/usr/local/directadmin/plugins/mail-login/plugin-settings.conf}"
|
||||
HELPER_COMMAND="${MAIL_LOGIN_HELPER_COMMAND:-/usr/local/hitme_plugins/mail-login-helper/bin/create-login-url}"
|
||||
SETTINGS_FILE="${MAIL_LOGIN_SETTINGS_FILE:-/usr/local/directadmin/plugins/mxautologin/plugin-settings.conf}"
|
||||
|
||||
config_value() {
|
||||
key="$1"
|
||||
@@ -119,14 +117,6 @@ KNOWN_HOSTS="${KNOWN_HOSTS:-$KEY_DIR/known_hosts}"
|
||||
SCAN_TIMEOUT="${MAIL_LOGIN_CONNECT_TIMEOUT_SECONDS:-$CONFIG_SCAN_TIMEOUT}"
|
||||
SCAN_TIMEOUT="${SCAN_TIMEOUT:-5}"
|
||||
|
||||
case "$MX_MODE" in
|
||||
1|2) ;;
|
||||
*)
|
||||
echo "MX_LOGIN_MODE must be 1 or 2" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$MX_PORT" in
|
||||
*[!0-9]*|'')
|
||||
echo "MAIL_LOGIN_SSH_PORT must be numeric" >&2
|
||||
@@ -154,7 +144,7 @@ chmod 700 "$KEY_DIR"
|
||||
|
||||
if [ ! -f "$KEY_PATH" ]; then
|
||||
umask 077
|
||||
ssh-keygen -q -t ed25519 -a 100 -N "" -f "$KEY_PATH" -C "KLUCZ PLUGINU mail-autologin DLA SERWERA $SERVER_LABEL"
|
||||
ssh-keygen -q -t ed25519 -a 100 -N "" -f "$KEY_PATH" -C "KLUCZ PLUGINU MX-Autologin DLA SERWERA $SERVER_LABEL"
|
||||
fi
|
||||
|
||||
if [ ! -f "$KEY_PATH.pub" ]; then
|
||||
@@ -170,14 +160,9 @@ pin_mx_host_key
|
||||
|
||||
PUB_KEY="$(cat "$KEY_PATH.pub")"
|
||||
BASE_OPTIONS="from=\"$SOURCE_IP\",restrict,no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-pty"
|
||||
AUTH_OPTIONS="$BASE_OPTIONS"
|
||||
|
||||
if [ "$MX_MODE" = "2" ]; then
|
||||
AUTH_OPTIONS="$BASE_OPTIONS,command=\"$HELPER_COMMAND\""
|
||||
else
|
||||
AUTH_OPTIONS="$BASE_OPTIONS"
|
||||
fi
|
||||
|
||||
COMMENT="KLUCZ PLUGINU mail-autologin DLA SERWERA $SERVER_LABEL"
|
||||
COMMENT="KLUCZ PLUGINU MX-Autologin DLA SERWERA $SERVER_LABEL"
|
||||
|
||||
cat <<EOF
|
||||
Private key:
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ ROOT_DIR="$(cd .. && pwd)"
|
||||
ARCHIVE_DIR="$ROOT_DIR/archives/$VERSION"
|
||||
mkdir -p "$ARCHIVE_DIR"
|
||||
|
||||
tar -czf "$ARCHIVE_DIR/mail-login.tar.gz" \
|
||||
tar -czf "$ARCHIVE_DIR/mxautologin.tar.gz" \
|
||||
--exclude='./.git' \
|
||||
--exclude='./tests' \
|
||||
--exclude='./scripts/package.sh' \
|
||||
@@ -22,4 +22,4 @@ tar -czf "$ARCHIVE_DIR/mail-login.tar.gz" \
|
||||
--exclude='./.DS_Store' \
|
||||
.
|
||||
|
||||
echo "$ARCHIVE_DIR/mail-login.tar.gz"
|
||||
echo "$ARCHIVE_DIR/mxautologin.tar.gz"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
BASE_DIR="/usr/local/hitme_plugins/mail-login"
|
||||
BASE_DIR="/usr/local/hitme_plugins/mxautologin"
|
||||
|
||||
if [ "${PURGE_MAIL_LOGIN_DATA:-0}" = "1" ]; then
|
||||
rm -rf "$BASE_DIR"
|
||||
echo "mail-login data purged"
|
||||
echo "MX-Autologin data purged"
|
||||
else
|
||||
echo "mail-login uninstalled; external data preserved at $BASE_DIR"
|
||||
echo "MX-Autologin uninstalled; external data preserved at $BASE_DIR"
|
||||
fi
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
BASE_DIR="/usr/local/hitme_plugins/mail-login"
|
||||
PLUGIN_DIR="/usr/local/directadmin/plugins/mail-login"
|
||||
BASE_DIR="/usr/local/hitme_plugins/mxautologin"
|
||||
PLUGIN_DIR="/usr/local/directadmin/plugins/mxautologin"
|
||||
|
||||
mkdir -p "$BASE_DIR/secrets" "$BASE_DIR/logs" "$BASE_DIR/state/rate-limit"
|
||||
chmod 700 "$BASE_DIR/secrets" "$BASE_DIR/logs" "$BASE_DIR/state" "$BASE_DIR/state/rate-limit"
|
||||
@@ -12,4 +12,4 @@ fi
|
||||
touch "$BASE_DIR/logs/audit.log"
|
||||
chmod 600 "$BASE_DIR/logs/audit.log"
|
||||
|
||||
echo "mail-login updated; configuration is read from $PLUGIN_DIR/plugin-settings.conf"
|
||||
echo "MX-Autologin updated; configuration is read from $PLUGIN_DIR/plugin-settings.conf"
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once PLUGIN_ROOT . '/mx-helper/cleanup-expired-login-urls.php';
|
||||
|
||||
test('cleanup deletes only expired login urls matching local mail-login state', function (): void {
|
||||
$state = [
|
||||
'timestamp' => 1782810000,
|
||||
'expires' => 1782810030,
|
||||
'correlation' => 'autologin-h4-mxtest-1782810000',
|
||||
'id' => 'HASHURLMATCH',
|
||||
'redirect_path' => '/',
|
||||
'client_ip' => '198.51.100.10',
|
||||
'ip_bound' => 1,
|
||||
'status' => 'created',
|
||||
];
|
||||
|
||||
$matching = [
|
||||
'id' => 'HASHURLMATCH',
|
||||
'created' => '2026-06-30T10:00:00Z',
|
||||
'expires' => '2026-06-30T10:00:30Z',
|
||||
'redirectURL' => '/',
|
||||
'allowNetworks' => ['198.51.100.10/32'],
|
||||
];
|
||||
$wrongRedirect = $matching;
|
||||
$wrongRedirect['id'] = 'HASHURLWRONG';
|
||||
$wrongRedirect['redirectURL'] = '/admin';
|
||||
$notExpired = $matching;
|
||||
$notExpired['id'] = 'HASHURLFRESH';
|
||||
$notExpired['expires'] = '2099-06-30T10:00:30Z';
|
||||
|
||||
assert_true(mailLoginCleanupShouldDelete($matching, [$state], strtotime('2026-06-30T10:01:00Z')));
|
||||
assert_false(mailLoginCleanupShouldDelete($wrongRedirect, [$state], strtotime('2026-06-30T10:01:00Z')));
|
||||
assert_false(mailLoginCleanupShouldDelete($notExpired, [$state], strtotime('2026-06-30T10:01:00Z')));
|
||||
|
||||
$stateWithoutId = $state;
|
||||
unset($stateWithoutId['id']);
|
||||
assert_false(mailLoginCleanupShouldDelete($matching, [$stateWithoutId], strtotime('2026-06-30T10:01:00Z')));
|
||||
});
|
||||
|
||||
test('cleanup rejects non-https directadmin api base url', function (): void {
|
||||
try {
|
||||
mailLoginCleanupValidateBaseUrl('http://127.0.0.1:2222');
|
||||
throw new RuntimeException('Expected http base URL to fail');
|
||||
} catch (InvalidArgumentException $e) {
|
||||
assert_contains('https', strtolower($e->getMessage()));
|
||||
}
|
||||
});
|
||||
@@ -19,6 +19,7 @@ test('directadmin context derives server name from first host label', function (
|
||||
assert_same('mxtest', $ctx->username());
|
||||
assert_same('h4.domena.pl', $ctx->sourceHost());
|
||||
assert_same('h4', $ctx->serverName());
|
||||
assert_same('h4.domena.pl', $ctx->auditServerName());
|
||||
});
|
||||
|
||||
test('directadmin context uses configured source server name and trusts local server name host', function (): void {
|
||||
@@ -33,6 +34,7 @@ test('directadmin context uses configured source server name and trusts local se
|
||||
|
||||
assert_same('h4.domena.pl', $ctx->sourceHost());
|
||||
assert_same('h4', $ctx->serverName());
|
||||
assert_same('h4.domena.pl', $ctx->auditServerName());
|
||||
});
|
||||
|
||||
test('directadmin context fails closed without directadmin username', function (): void {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once PLUGIN_ROOT . '/exec/lib/Http.php';
|
||||
|
||||
test('http redirect page performs top level browser navigation', function (): void {
|
||||
$html = Http::redirectPageHtml('https://mx1.domena.pl:2222/api/login/url?key=SECRET&x=1');
|
||||
|
||||
assert_contains('window.top.location.replace', $html);
|
||||
assert_contains('window.location.replace', $html);
|
||||
assert_contains('<meta http-equiv="refresh"', $html);
|
||||
assert_contains('https://mx1.domena.pl:2222/api/login/url?key=SECRET&x=1', $html);
|
||||
assert_contains('https://mx1.domena.pl:2222/api/login/url?key=SECRET\\u0026x=1', $html);
|
||||
});
|
||||
|
||||
test('http raw response is a complete http response for directadmin proxy', function (): void {
|
||||
$response = Http::rawHttpResponse(200, [
|
||||
'Content-Type' => 'text/html; charset=UTF-8',
|
||||
'Cache-Control' => 'no-store, private',
|
||||
], '<!doctype html><html></html>');
|
||||
|
||||
assert_contains("HTTP/1.1 200 OK\r\n", $response);
|
||||
assert_contains("Content-Type: text/html; charset=UTF-8\r\n", $response);
|
||||
assert_contains("Content-Length: 28\r\n", $response);
|
||||
assert_contains("\r\n\r\n<!doctype html><html></html>", $response);
|
||||
});
|
||||
@@ -21,7 +21,7 @@ test('keygen script creates plugin key and direct ssh authorized keys line', fun
|
||||
assert_contains('from="203.0.113.10"', $output);
|
||||
assert_contains('restrict,no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-pty', $output);
|
||||
assert_not_contains('command=', $output);
|
||||
assert_contains('KLUCZ PLUGINU mail-autologin DLA SERWERA h4', $output);
|
||||
assert_contains('KLUCZ PLUGINU MX-Autologin DLA SERWERA h4', $output);
|
||||
});
|
||||
|
||||
test('keygen script detects source ip without parameters', function (): void {
|
||||
@@ -92,8 +92,8 @@ test('keygen script pins mx host key from plugin settings', function (): void {
|
||||
assert_contains('Pinned MX host key:', $output);
|
||||
});
|
||||
|
||||
test('keygen script prints helper forced command line when mx login mode is helper', function (): void {
|
||||
$keyDir = TEST_TMP . '/keys-helper';
|
||||
test('keygen script ignores helper mode variables and prints direct ssh line only', function (): void {
|
||||
$keyDir = TEST_TMP . '/keys-direct-only';
|
||||
$script = PLUGIN_ROOT . '/scripts/keygen.sh';
|
||||
$cmd = sprintf(
|
||||
'MAIL_LOGIN_KEY_DIR=%s MX_LOGIN_MODE=2 SERVERNAME=%s SOURCE_IP=%s %s',
|
||||
@@ -107,6 +107,7 @@ test('keygen script prints helper forced command line when mx login mode is help
|
||||
|
||||
assert_same(0, $code, implode("\n", $out));
|
||||
$output = implode("\n", $out);
|
||||
assert_contains('command="/usr/local/hitme_plugins/mail-login-helper/bin/create-login-url"', $output);
|
||||
assert_contains('KLUCZ PLUGINU mail-autologin DLA SERWERA h4', $output);
|
||||
assert_not_contains('command=', $output);
|
||||
assert_not_contains('mail-login-helper', $output);
|
||||
assert_contains('KLUCZ PLUGINU MX-Autologin DLA SERWERA h4', $output);
|
||||
});
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
test('mx helper conditionally passes user ip binding to da login-url', function (): void {
|
||||
$bin = TEST_TMP . '/fake-da';
|
||||
$log = TEST_TMP . '/fake-da.log';
|
||||
$audit = TEST_TMP . '/audit.log';
|
||||
$state = TEST_TMP . '/state';
|
||||
|
||||
test_write($bin, "#!/bin/sh\nprintf '%s\\n' \"$@\" >> " . escapeshellarg($log) . "\necho 'URL: https://mx1.domena.pl:2222/api/login/url?key=SECRET'\n");
|
||||
chmod($bin, 0755);
|
||||
|
||||
$script = PLUGIN_ROOT . '/mx-helper/mail-login-authorized-command.sh';
|
||||
$cmd = sprintf(
|
||||
'MAIL_LOGIN_DA_BIN=%s MAIL_LOGIN_AUDIT_LOG=%s MAIL_LOGIN_STATE_DIR=%s MAIL_LOGIN_ALLOWED_SOURCE_HOSTS=%s %s %s',
|
||||
escapeshellarg($bin),
|
||||
escapeshellarg($audit),
|
||||
escapeshellarg($state),
|
||||
escapeshellarg('h4.domena.pl'),
|
||||
escapeshellarg($script),
|
||||
'mail-login-create-url mxtest h4.domena.pl h4 30 1 198.51.100.10 / 1782810000'
|
||||
);
|
||||
exec($cmd, $out, $code);
|
||||
assert_same(0, $code, implode("\n", $out));
|
||||
assert_contains('--ip=198.51.100.10', file_get_contents($log) ?: '');
|
||||
assert_contains('autologin-h4-mxtest-', file_get_contents($audit) ?: '');
|
||||
|
||||
file_put_contents($log, '');
|
||||
$cmdNoIp = sprintf(
|
||||
'MAIL_LOGIN_DA_BIN=%s MAIL_LOGIN_AUDIT_LOG=%s MAIL_LOGIN_STATE_DIR=%s MAIL_LOGIN_ALLOWED_SOURCE_HOSTS=%s %s %s',
|
||||
escapeshellarg($bin),
|
||||
escapeshellarg($audit),
|
||||
escapeshellarg($state),
|
||||
escapeshellarg('h4.domena.pl'),
|
||||
escapeshellarg($script),
|
||||
'mail-login-create-url mxtest h4.domena.pl h4 30 0 198.51.100.10 / 1782810001'
|
||||
);
|
||||
exec($cmdNoIp, $outNoIp, $codeNoIp);
|
||||
assert_same(0, $codeNoIp, implode("\n", $outNoIp));
|
||||
assert_not_contains('--ip=', file_get_contents($log) ?: '');
|
||||
assert_contains('ip_bound_disabled', file_get_contents($audit) ?: '');
|
||||
});
|
||||
|
||||
test('mx helper accepts forced command arguments from ssh original command', function (): void {
|
||||
$bin = TEST_TMP . '/fake-da-original';
|
||||
$log = TEST_TMP . '/fake-da-original.log';
|
||||
$audit = TEST_TMP . '/audit-original.log';
|
||||
$state = TEST_TMP . '/state-original';
|
||||
|
||||
test_write($bin, "#!/bin/sh\nprintf '%s\\n' \"$@\" >> " . escapeshellarg($log) . "\necho 'URL: https://mx1.domena.pl:2222/api/login/url?key=SECRET'\n");
|
||||
chmod($bin, 0755);
|
||||
|
||||
$script = PLUGIN_ROOT . '/mx-helper/mail-login-authorized-command.sh';
|
||||
$cmd = sprintf(
|
||||
'MAIL_LOGIN_DA_BIN=%s MAIL_LOGIN_AUDIT_LOG=%s MAIL_LOGIN_STATE_DIR=%s MAIL_LOGIN_ALLOWED_SOURCE_HOSTS=%s SSH_ORIGINAL_COMMAND=%s %s',
|
||||
escapeshellarg($bin),
|
||||
escapeshellarg($audit),
|
||||
escapeshellarg($state),
|
||||
escapeshellarg('h4.domena.pl'),
|
||||
escapeshellarg('mail-login-create-url mxtest h4.domena.pl evil 30 1 198.51.100.10 / 1782810002'),
|
||||
escapeshellarg($script)
|
||||
);
|
||||
exec($cmd, $out, $code);
|
||||
|
||||
assert_same(0, $code, implode("\n", $out));
|
||||
assert_contains('--user=mxtest', file_get_contents($log) ?: '');
|
||||
assert_contains('autologin-h4-mxtest-', file_get_contents($audit) ?: '');
|
||||
assert_not_contains('autologin-evil-mxtest-', file_get_contents($audit) ?: '');
|
||||
});
|
||||
|
||||
test('mx helper rejects disallowed source hosts and invalid client ips', function (): void {
|
||||
$bin = TEST_TMP . '/fake-da-reject';
|
||||
$audit = TEST_TMP . '/audit-reject.log';
|
||||
$state = TEST_TMP . '/state-reject';
|
||||
test_write($bin, "#!/bin/sh\necho 'URL: https://mx1.domena.pl:2222/api/login/url?key=SECRET'\n");
|
||||
chmod($bin, 0755);
|
||||
$script = PLUGIN_ROOT . '/mx-helper/mail-login-authorized-command.sh';
|
||||
|
||||
$badHost = sprintf(
|
||||
'MAIL_LOGIN_DA_BIN=%s MAIL_LOGIN_AUDIT_LOG=%s MAIL_LOGIN_STATE_DIR=%s MAIL_LOGIN_ALLOWED_SOURCE_HOSTS=%s %s %s 2>/dev/null',
|
||||
escapeshellarg($bin),
|
||||
escapeshellarg($audit),
|
||||
escapeshellarg($state),
|
||||
escapeshellarg('h4.domena.pl'),
|
||||
escapeshellarg($script),
|
||||
'mail-login-create-url mxtest evil.domena.pl evil 30 1 198.51.100.10 / 1782810003'
|
||||
);
|
||||
exec($badHost, $outHost, $codeHost);
|
||||
assert_same(1, $codeHost);
|
||||
|
||||
$badIp = sprintf(
|
||||
'MAIL_LOGIN_DA_BIN=%s MAIL_LOGIN_AUDIT_LOG=%s MAIL_LOGIN_STATE_DIR=%s MAIL_LOGIN_ALLOWED_SOURCE_HOSTS=%s %s %s 2>/dev/null',
|
||||
escapeshellarg($bin),
|
||||
escapeshellarg($audit),
|
||||
escapeshellarg($state),
|
||||
escapeshellarg('h4.domena.pl'),
|
||||
escapeshellarg($script),
|
||||
'mail-login-create-url mxtest h4.domena.pl h4 30 1 999.999.999.999 / 1782810004'
|
||||
);
|
||||
exec($badIp, $outIp, $codeIp);
|
||||
assert_same(1, $codeIp);
|
||||
assert_not_contains("\nBROKEN", file_get_contents($audit) ?: '');
|
||||
});
|
||||
@@ -5,20 +5,20 @@ test('plugin metadata and packaging docs match project rules', function (): void
|
||||
$conf = file_get_contents(PLUGIN_ROOT . '/plugin.conf') ?: '';
|
||||
$packing = file_get_contents(PROJECT_ROOT . '/PACKING.md') ?: '';
|
||||
|
||||
assert_contains('name=mail-login', $conf);
|
||||
assert_contains('id=mail-login', $conf);
|
||||
assert_contains('name=MX-Autologin', $conf);
|
||||
assert_contains('id=mxautologin', $conf);
|
||||
assert_contains('type=user', $conf);
|
||||
assert_contains('version=1.0.9', $conf);
|
||||
assert_contains('version=1.1.6', $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);
|
||||
assert_contains('/Users/marek/GPT/ChatGPT/da_plugins/mxautologin/src', $packing);
|
||||
assert_contains('/Users/marek/GPT/ChatGPT/da_plugins/mxautologin/archives/X.Y.Z/mxautologin.tar.gz', $packing);
|
||||
});
|
||||
|
||||
test('package source excludes local-only files and dangerous placeholders', function (): void {
|
||||
$package = file_get_contents(PLUGIN_ROOT . '/scripts/package.sh') ?: '';
|
||||
assert_contains("--exclude='./.git'", $package);
|
||||
assert_contains("--exclude='./tests'", $package);
|
||||
assert_contains('mail-login.tar.gz', $package);
|
||||
assert_contains('mxautologin.tar.gz', $package);
|
||||
assert_true(is_file(PLUGIN_ROOT . '/scripts/keygen.sh'));
|
||||
|
||||
$bad = [];
|
||||
@@ -38,3 +38,55 @@ test('package source excludes local-only files and dangerous placeholders', func
|
||||
}
|
||||
assert_same([], $bad);
|
||||
});
|
||||
|
||||
test('package source contains no mx helper implementation or mode switch', function (): void {
|
||||
assert_false(is_dir(PLUGIN_ROOT . '/mx-helper'), 'mx-helper directory must not be shipped');
|
||||
|
||||
$forbidden = [];
|
||||
$needles = ['MX_LOGIN_MODE', 'mail-login-helper', 'mail-login-create-url', '/mx-helper/'];
|
||||
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(PLUGIN_ROOT, FilesystemIterator::SKIP_DOTS));
|
||||
foreach ($it as $file) {
|
||||
if (!$file->isFile()) {
|
||||
continue;
|
||||
}
|
||||
$relative = substr($file->getPathname(), strlen(PLUGIN_ROOT) + 1);
|
||||
if (str_starts_with($relative, '.git/') || str_starts_with($relative, 'tests/')) {
|
||||
continue;
|
||||
}
|
||||
$content = file_get_contents($file->getPathname()) ?: '';
|
||||
foreach ($needles as $needle) {
|
||||
if (str_contains($relative, $needle) || str_contains($content, $needle)) {
|
||||
$forbidden[] = $relative . ':' . $needle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert_same([], $forbidden);
|
||||
});
|
||||
|
||||
test('directadmin entry points use mx autologin directory and display label', function (): void {
|
||||
$txt = file_get_contents(PLUGIN_ROOT . '/hooks/user_txt.html') ?: '';
|
||||
$img = file_get_contents(PLUGIN_ROOT . '/hooks/user_img.html') ?: '';
|
||||
$login = file_get_contents(PLUGIN_ROOT . '/user/login.raw') ?: '';
|
||||
$index = file_get_contents(PLUGIN_ROOT . '/user/index.html') ?: '';
|
||||
|
||||
assert_contains('/CMD_PLUGINS/mxautologin/login.raw', $txt);
|
||||
assert_contains('Zaloguj do serwera poczty', $txt);
|
||||
assert_contains('/CMD_PLUGINS/mxautologin/images/user_icon.svg', $img);
|
||||
assert_contains('alt="Zaloguj do serwera poczty"', $img);
|
||||
assert_contains('/usr/local/directadmin/plugins/mxautologin/php.ini', $login);
|
||||
assert_contains('/CMD_PLUGINS/mxautologin/login.raw', $index);
|
||||
});
|
||||
|
||||
test('plugin icon follows directadmin login screen palette', function (): void {
|
||||
$icon = file_get_contents(PLUGIN_ROOT . '/images/user_icon.svg') ?: '';
|
||||
|
||||
assert_contains('#6670d6', $icon);
|
||||
assert_contains('#4f96c8', $icon);
|
||||
assert_contains('#2ec4b6', $icon);
|
||||
assert_contains('#2f95c8', $icon);
|
||||
assert_contains('#ff8a00', $icon);
|
||||
assert_contains('fill="#ff8a00"', $icon);
|
||||
assert_contains('#ffffff', $icon);
|
||||
assert_not_contains('#14532d', $icon);
|
||||
});
|
||||
|
||||
@@ -22,36 +22,28 @@ test('remote login url client builds direct ssh command by default', function ()
|
||||
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_not_contains('MX_LOGIN_MODE', 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 {
|
||||
test('remote login url client omits expiry when ttl is zero', 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',
|
||||
'LOGIN_KEY_TTL' => '0',
|
||||
]);
|
||||
$request = new LoginUrlRequest('mxtest', 'h4.domena.pl', 'h4', 30, true, '198.51.100.10', '/', 1782810000);
|
||||
$request = new LoginUrlRequest('mxtest', 'h4.domena.pl', 'h4', 0, 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]);
|
||||
assert_same('1', $command[$pos + 5]);
|
||||
assert_same('1782810000', $command[$pos + 8]);
|
||||
assert_not_contains('--expiry=', implode(' ', $command));
|
||||
});
|
||||
|
||||
test('remote login url client validates helper output before returning it', function (): void {
|
||||
test('remote login url client validates direct ssh output before returning it', function (): void {
|
||||
$settings = Settings::fromArray(['MAIL_LOGIN_TARGET_BASE_URL' => 'https://mx1.domena.pl:2222']);
|
||||
$request = new LoginUrlRequest('mxtest', 'h4.domena.pl', 'h4', 30, true, '198.51.100.10', '/', 1782810000);
|
||||
|
||||
@@ -67,7 +59,7 @@ test('remote login url client validates helper output before returning it', func
|
||||
|
||||
try {
|
||||
$badClient->create($settings, $request);
|
||||
throw new RuntimeException('Expected invalid helper URL to fail');
|
||||
throw new RuntimeException('Expected invalid direct SSH URL to fail');
|
||||
} catch (RuntimeException $e) {
|
||||
assert_contains('login url', strtolower($e->getMessage()));
|
||||
}
|
||||
@@ -76,7 +68,6 @@ test('remote login url client validates helper output before returning it', func
|
||||
test('remote login url client reports direct ssh failure diagnostics', function (): void {
|
||||
$settings = Settings::fromArray([
|
||||
'MAIL_LOGIN_TARGET_BASE_URL' => 'https://mx1.domena.pl:2222',
|
||||
'MX_LOGIN_MODE' => '1',
|
||||
]);
|
||||
$request = new LoginUrlRequest('mxtest', 'h4.domena.pl', 'h4', 30, true, '198.51.100.10', '/', 1782810000);
|
||||
$client = new RemoteLoginUrlClient(function (array $command, int $timeout): ProcessResult {
|
||||
|
||||
+33
-25
@@ -6,23 +6,21 @@ require_once PLUGIN_ROOT . '/exec/lib/Settings.php';
|
||||
test('settings expose secure defaults from plugin-settings.conf', function (): void {
|
||||
$settings = Settings::fromArray([]);
|
||||
|
||||
assert_same(30, $settings->loginKeyTtl());
|
||||
assert_same(14400, $settings->loginKeyTtl());
|
||||
assert_true($settings->userIpBound());
|
||||
assert_true($settings->ipMask());
|
||||
assert_same('MASKED', $settings->auditClientIp('198.51.100.10'));
|
||||
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());
|
||||
});
|
||||
|
||||
test('settings runtime file is the directadmin plugin settings file', function (): void {
|
||||
assert_same('/usr/local/directadmin/plugins/mail-login/plugin-settings.conf', Settings::EXTERNAL_SETTINGS);
|
||||
assert_same('/usr/local/directadmin/plugins/mxautologin/plugin-settings.conf', Settings::EXTERNAL_SETTINGS);
|
||||
});
|
||||
|
||||
test('settings reject login key ttl outside production range', function (): void {
|
||||
foreach (['4', '301', 'abc'] as $ttl) {
|
||||
foreach (['4', '86401', 'abc'] as $ttl) {
|
||||
try {
|
||||
Settings::fromArray(['LOGIN_KEY_TTL' => $ttl]);
|
||||
throw new RuntimeException('Expected invalid TTL to fail: ' . $ttl);
|
||||
@@ -37,14 +35,18 @@ test('settings accept explicit ttl and user ip binding toggle', function (): voi
|
||||
'LOGIN_KEY_TTL' => '5',
|
||||
'USER_IP_BOUND' => '0',
|
||||
'IP_MASK' => '0',
|
||||
'MX_LOGIN_MODE' => '2',
|
||||
]);
|
||||
|
||||
assert_same(5, $settings->loginKeyTtl());
|
||||
assert_false($settings->userIpBound());
|
||||
assert_false($settings->ipMask());
|
||||
assert_same('198.51.100.10', $settings->auditClientIp('198.51.100.10'));
|
||||
assert_same(2, $settings->mxLoginMode());
|
||||
});
|
||||
|
||||
test('settings accept zero login key ttl as directadmin default expiry', function (): void {
|
||||
$settings = Settings::fromArray(['LOGIN_KEY_TTL' => '0']);
|
||||
|
||||
assert_same(0, $settings->loginKeyTtl());
|
||||
});
|
||||
|
||||
test('settings reject unsupported ip mask values', function (): void {
|
||||
@@ -58,17 +60,6 @@ test('settings reject unsupported ip mask values', function (): void {
|
||||
}
|
||||
});
|
||||
|
||||
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('source plugin settings do not expose local source host allowlist', function (): void {
|
||||
$defaults = Settings::defaults();
|
||||
$config = file_get_contents(PLUGIN_ROOT . '/plugin-settings.conf') ?: '';
|
||||
@@ -77,14 +68,31 @@ test('source plugin settings do not expose local source host allowlist', functio
|
||||
assert_not_contains('MAIL_LOGIN_ALLOWED_SOURCE_HOSTS', $config);
|
||||
});
|
||||
|
||||
test('settings reject disabled plugin and unsupported methods through access guard', function (): void {
|
||||
try {
|
||||
Settings::fromArray(['MAIL_LOGIN_METHOD' => 'password']);
|
||||
throw new RuntimeException('Expected unsupported method to fail');
|
||||
} catch (InvalidArgumentException $e) {
|
||||
assert_contains('MAIL_LOGIN_METHOD', $e->getMessage());
|
||||
}
|
||||
test('settings do not expose mx helper mode', function (): void {
|
||||
$defaults = Settings::defaults();
|
||||
$config = file_get_contents(PLUGIN_ROOT . '/plugin-settings.conf') ?: '';
|
||||
|
||||
assert_false(array_key_exists('MX_LOGIN_MODE', $defaults));
|
||||
assert_not_contains('MX_LOGIN_MODE', $config);
|
||||
});
|
||||
|
||||
test('settings do not expose deprecated mail login method switch', function (): void {
|
||||
$defaults = Settings::defaults();
|
||||
$config = file_get_contents(PLUGIN_ROOT . '/plugin-settings.conf') ?: '';
|
||||
|
||||
assert_false(array_key_exists('MAIL_LOGIN_METHOD', $defaults));
|
||||
assert_not_contains('MAIL_LOGIN_METHOD', $config);
|
||||
});
|
||||
|
||||
test('settings do not expose cosmetic target name setting', function (): void {
|
||||
$defaults = Settings::defaults();
|
||||
$config = file_get_contents(PLUGIN_ROOT . '/plugin-settings.conf') ?: '';
|
||||
|
||||
assert_false(array_key_exists('MAIL_LOGIN_TARGET_NAME', $defaults));
|
||||
assert_not_contains('MAIL_LOGIN_TARGET_NAME', $config);
|
||||
});
|
||||
|
||||
test('settings expose disabled plugin flag through access guard', function (): void {
|
||||
$settings = Settings::fromArray(['DEFAULT_ENABLE' => 'false']);
|
||||
assert_false($settings->defaultEnable());
|
||||
});
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/mail-login/php.ini
|
||||
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/mxautologin/php.ini
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
@@ -7,6 +7,6 @@ header('Cache-Control: no-store, private');
|
||||
header('Referrer-Policy: no-referrer');
|
||||
echo '<!doctype html><html lang="pl"><head><meta charset="utf-8">';
|
||||
echo '<meta name="viewport" content="width=device-width, initial-scale=1">';
|
||||
echo '<title>Serwer poczty</title></head><body>';
|
||||
echo '<p><a href="/CMD_PLUGINS/mail-login/login.raw">Zaloguj do serwera poczty</a></p>';
|
||||
echo '<title>Zaloguj do serwera poczty</title></head><body>';
|
||||
echo '<p><a href="/CMD_PLUGINS/mxautologin/login.raw">Zaloguj do serwera poczty</a></p>';
|
||||
echo '</body></html>';
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/mail-login/php.ini
|
||||
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/mxautologin/php.ini
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user