diff --git a/exec/lib/RemoteLoginUrlClient.php b/exec/lib/RemoteLoginUrlClient.php index 6ac1539..7cb0b5c 100644 --- a/exec/lib/RemoteLoginUrlClient.php +++ b/exec/lib/RemoteLoginUrlClient.php @@ -61,33 +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, - ]); - if ($request->ttl > 0) { - $command[] = '--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 @@ -101,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); @@ -112,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; @@ -140,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); @@ -161,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); diff --git a/exec/lib/Settings.php b/exec/lib/Settings.php index 7b4058d..0e86729 100644 --- a/exec/lib/Settings.php +++ b/exec/lib/Settings.php @@ -58,7 +58,6 @@ 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' => '/', @@ -116,11 +115,6 @@ 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', '/'); @@ -267,9 +261,6 @@ 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/mx-helper/cleanup-expired-login-urls.php b/mx-helper/cleanup-expired-login-urls.php deleted file mode 100755 index e089910..0000000 --- a/mx-helper/cleanup-expired-login-urls.php +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/local/bin/php - 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> - */ -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 $entry - * @param list> $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 $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 $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); -} diff --git a/mx-helper/install-helper.sh b/mx-helper/install-helper.sh deleted file mode 100755 index c1b2aae..0000000 --- a/mx-helper/install-helper.sh +++ /dev/null @@ -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" diff --git a/mx-helper/mail-login-authorized-command.sh b/mx-helper/mail-login-authorized-command.sh deleted file mode 100755 index b594138..0000000 --- a/mx-helper/mail-login-authorized-command.sh +++ /dev/null @@ -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" diff --git a/plugin-settings.conf b/plugin-settings.conf index 3f7fc31..26e85a0 100644 --- a/plugin-settings.conf +++ b/plugin-settings.conf @@ -8,10 +8,8 @@ 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 +# 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. # Nazwa i adres docelowego panelu poczty. MAIL_LOGIN_TARGET_NAME=mx1 diff --git a/plugin.conf b/plugin.conf index 5190e09..bfae10f 100644 --- a/plugin.conf +++ b/plugin.conf @@ -2,7 +2,7 @@ name=MX-Autologin id=mxautologin type=user author=HITME.PL -version=1.0.14 +version=1.1.1 active=no installed=no user_run_as=root diff --git a/scripts/keygen.sh b/scripts/keygen.sh index 01d982d..eee5549 100755 --- a/scripts/keygen.sh +++ b/scripts/keygen.sh @@ -4,9 +4,7 @@ set -eu 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/mxautologin/plugin-settings.conf}" -HELPER_COMMAND="${MAIL_LOGIN_HELPER_COMMAND:-/usr/local/hitme_plugins/mail-login-helper/bin/create-login-url}" 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 @@ -170,12 +160,7 @@ 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" - -if [ "$MX_MODE" = "2" ]; then - AUTH_OPTIONS="$BASE_OPTIONS,command=\"$HELPER_COMMAND\"" -else - AUTH_OPTIONS="$BASE_OPTIONS" -fi +AUTH_OPTIONS="$BASE_OPTIONS" COMMENT="KLUCZ PLUGINU MX-Autologin DLA SERWERA $SERVER_LABEL" diff --git a/tests/cleanup_test.php b/tests/cleanup_test.php deleted file mode 100644 index 86a8b35..0000000 --- a/tests/cleanup_test.php +++ /dev/null @@ -1,48 +0,0 @@ - 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())); - } -}); diff --git a/tests/keygen_script_test.php b/tests/keygen_script_test.php index 7f1525c..76141a0 100644 --- a/tests/keygen_script_test.php +++ b/tests/keygen_script_test.php @@ -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_not_contains('command=', $output); + assert_not_contains('mail-login-helper', $output); assert_contains('KLUCZ PLUGINU MX-Autologin DLA SERWERA h4', $output); }); diff --git a/tests/mx_helper_test.php b/tests/mx_helper_test.php deleted file mode 100644 index 506e6df..0000000 --- a/tests/mx_helper_test.php +++ /dev/null @@ -1,103 +0,0 @@ -> " . 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) ?: ''); -}); diff --git a/tests/package_contents_test.php b/tests/package_contents_test.php index f93002b..aea1d8c 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=MX-Autologin', $conf); assert_contains('id=mxautologin', $conf); assert_contains('type=user', $conf); - assert_contains('version=1.0.14', $conf); + assert_contains('version=1.1.1', $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/mxautologin.tar.gz', $packing); @@ -39,6 +39,31 @@ 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') ?: ''; diff --git a/tests/remote_login_url_client_test.php b/tests/remote_login_url_client_test.php index c0ac2da..1b7fdf3 100644 --- a/tests/remote_login_url_client_test.php +++ b/tests/remote_login_url_client_test.php @@ -22,35 +22,13 @@ 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 { - $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]); - assert_same('1', $command[$pos + 5]); - assert_same('1782810000', $command[$pos + 8]); -}); - 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', @@ -65,7 +43,7 @@ test('remote login url client omits expiry when ttl is zero', function (): void 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); @@ -81,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())); } @@ -90,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 { diff --git a/tests/settings_test.php b/tests/settings_test.php index 9b66e1c..881ef1e 100644 --- a/tests/settings_test.php +++ b/tests/settings_test.php @@ -12,7 +12,6 @@ test('settings expose secure defaults from plugin-settings.conf', function (): v 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()); }); @@ -37,14 +36,12 @@ 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 { @@ -64,17 +61,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') ?: ''; @@ -83,6 +69,14 @@ test('source plugin settings do not expose local source host allowlist', functio assert_not_contains('MAIL_LOGIN_ALLOWED_SOURCE_HOSTS', $config); }); +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 reject disabled plugin and unsupported methods through access guard', function (): void { try { Settings::fromArray(['MAIL_LOGIN_METHOD' => 'password']);