From df30fcebb6b306ba22bff577fb084d1190f0e638 Mon Sep 17 00:00:00 2001 From: kalugin66 Date: Thu, 27 Aug 2026 11:43:56 +0500 Subject: [PATCH] ssd error --- .env.example | 5 +++ lib/config.js | 2 + lib/ldapClient.js | 4 +- lib/passwordSetter.js | 88 ++++++++++++++++++++++++++++--------------- 4 files changed, 68 insertions(+), 31 deletions(-) diff --git a/.env.example b/.env.example index 5560686..918ef7d 100644 --- a/.env.example +++ b/.env.example @@ -55,6 +55,11 @@ ENCRYPT_TRAFFIC=false # (comma-separated). Default: only the host from LDAP_URL is used, # the rest is discovered via DNS SRV (_kerberos._tcp.). LDAP_KDC_SERVERS=dc.school25.ru,dc3.school25.ru,srv.school25.ru +# Transient GSSAPI/KDC errors (e.g. "Server (ldap/...@REALM) unknown") are +# retried automatically. Number of attempts (default 3) and pause between them +# in milliseconds (default 1000). Applies to the kerberos password method. +# LDAP_PASSWORD_RETRIES=3 +# LDAP_PASSWORD_RETRY_DELAY_MS=1000 # ============ Email (registration) ============ # SMTP server used to send verification codes and registration result letters. diff --git a/lib/config.js b/lib/config.js index cc0103c..0a739d4 100644 --- a/lib/config.js +++ b/lib/config.js @@ -27,6 +27,8 @@ const config = { passwordMethod: getEnv('LDAP_PASSWORD_METHOD', passwordMethodDefault), ldapRealm: getEnv('LDAP_REALM', String(getEnv('LDAP_DOMAIN', 'school25.ru')).toUpperCase()), kdcServers: getEnv('LDAP_KDC_SERVERS', ''), + passwordRetries: Math.max(1, parseInt(getEnv('LDAP_PASSWORD_RETRIES', '3'), 10) || 3), + passwordRetryDelayMs: Math.max(0, parseInt(getEnv('LDAP_PASSWORD_RETRY_DELAY_MS', '1000'), 10) || 1000), powerShellExe: getEnv('LDAP_POWERSHELL_EXE', 'powershell.exe'), emailHost: getEnv('EMAIL_HOST', ''), emailPort: parseInt(getEnv('EMAIL_PORT', '587'), 10), diff --git a/lib/ldapClient.js b/lib/ldapClient.js index 955cd97..1a5bb47 100644 --- a/lib/ldapClient.js +++ b/lib/ldapClient.js @@ -248,7 +248,9 @@ async function setPassword(client, userDN, password) { await passwordSetter.setPasswordViaKerberos(userDN, password, { ldapUrl: config.ldapUrl, principal, - adminPassword: config.adminPassword + adminPassword: config.adminPassword, + retries: config.passwordRetries, + retryDelayMs: config.passwordRetryDelayMs }); return; } diff --git a/lib/passwordSetter.js b/lib/passwordSetter.js index 53afd36..f6a6e3b 100644 --- a/lib/passwordSetter.js +++ b/lib/passwordSetter.js @@ -173,48 +173,76 @@ async function setPasswordViaKerberos(userDN, password, { ldapUrl = '', principal = '', adminPassword = '', - ccache = '/tmp/krb5cc_ldapauth' + ccache = '/tmp/krb5cc_ldapauth', + retries = 3, + retryDelayMs = 1000 } = {}) { if (!principal) { throw new Error('Не задан principal администратора (ADMIN_LOGIN/LDAP_REALM) для Kerberos'); } + const maxAttempts = retries >= 1 ? retries : 3; const ccacheEnv = { KRB5CCNAME: ccache }; - const kinit = await runChild(exe, ['-c', ccache, principal], { - env: ccacheEnv, - input: String(adminPassword || '') + '\n', - timeoutMs, - describe: 'kinit' - }); - if (kinit.code !== 0) { - const detail = String(kinit.stderr || kinit.stdout || '').trim(); - throw new Error(`Не удалось получить билет Kerberos (kinit, код ${kinit.code})${detail ? ': ' + detail : ''}`); + async function destroyCache() { + try { + await runChild('kdestroy', ['-c', ccache], { + env: ccacheEnv, + input: '', + timeoutMs: 5000, + describe: 'kdestroy' + }); + } catch (_) { /* best-effort cleanup */ } } - const ldapArgs = ['-Y', 'GSSAPI']; - if (ldapUrl) ldapArgs.push('-H', ldapUrl); - ldapArgs.push('-O', 'maxssf=256'); + // Transient GSSAPI/KDC errors (e.g. "Server (ldap/...@REALM) unknown" from + // ldapmodify) often pass on retry — DNS canonicalization or DC selection may + // differ. Each attempt gets a fresh kinit ticket so a partial/poisoned + // ccache can't cause repeated failures. Rewriting the same unicodePwd is + // idempotent, so retrying is safe. + let lastError = null; - const ldapmodify = await runChild(ldapmodifyExe, ldapArgs, { - env: ccacheEnv, - input: buildUnicodePwdLdif(userDN, password), - timeoutMs, - describe: 'ldapmodify' - }); + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const kinit = await runChild(exe, ['-c', ccache, principal], { + env: ccacheEnv, + input: String(adminPassword || '') + '\n', + timeoutMs, + describe: 'kinit' + }); + if (kinit.code !== 0) { + const detail = String(kinit.stderr || kinit.stdout || '').trim(); + throw new Error(`Не удалось получить билет Kerberos (kinit, код ${kinit.code})${detail ? ': ' + detail : ''}`); + } - try { - await runChild('kdestroy', ['-c', ccache], { - env: ccacheEnv, - input: '', - timeoutMs: 5000, - describe: 'kdestroy' - }); - } catch (_) { /* best-effort cleanup */ } + const ldapArgs = ['-Y', 'GSSAPI']; + if (ldapUrl) ldapArgs.push('-H', ldapUrl); + ldapArgs.push('-O', 'maxssf=256'); - if (ldapmodify.code !== 0) { - const detail = String(ldapmodify.stderr || ldapmodify.stdout || '').trim(); - throw new Error(`Ошибка установки пароля через Kerberos/GSSAPI (код ${ldapmodify.code})${detail ? ': ' + detail : ''}`); + const ldapmodify = await runChild(ldapmodifyExe, ldapArgs, { + env: ccacheEnv, + input: buildUnicodePwdLdif(userDN, password), + timeoutMs, + describe: 'ldapmodify' + }); + + if (ldapmodify.code !== 0) { + const detail = String(ldapmodify.stderr || ldapmodify.stdout || '').trim(); + throw new Error(`Ошибка установки пароля через Kerberos/GSSAPI (код ${ldapmodify.code})${detail ? ': ' + detail : ''}`); + } + + await destroyCache(); + return; + } catch (err) { + lastError = err; + await destroyCache(); + if (attempt < maxAttempts) { + console.warn(`⚠ Установка пароля через Kerberos: попытка ${attempt}/${maxAttempts} не удалась, повтор через ${retryDelayMs} мс: ${err.message}`); + await new Promise((resolve) => setTimeout(resolve, retryDelayMs)); + } + } } + + throw lastError; } module.exports = {