Add detailed debug logging for the login flow
The login flow was near-silent: three info lines with no way to see why a session was considered expired or what happened to the cookies. Debugging a failing login meant guessing. Every login step now logs on debug level with a `phase` field: the login page fetch, the extracted nonce, the POST to /login/api/login.php, the session check, and any request retried after a re-login. Each of those requests is bracketed by a cookie snapshot taken before it is sent and one taken after, plus a delta of added/changed/removed cookies. The "before" snapshot is logged prior to sending, so it survives a hanging request. isLoginRequiredResponse() is split so the criterion that matched (status-401, login-markup, ...) can be reported instead of just a boolean. Secrets stay out of the log: the password is replaced by ***, and cookie values - including those in the raw Set-Cookie header - are truncated to their first four characters plus length. That still shows whether a session id changed without putting the session itself in the log. Detail logging is strictly debug-level via logLoginDetail(); the existing logDebug() falls back to info, which would flood a plain info logger. When no debug logger is attached, no snapshot is taken and nothing is serialized. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vendored
+185
-9
@@ -486,6 +486,14 @@ class AgrarmonitorConnector {
|
||||
throw new Error('Agrarmonitor-Credentials nicht konfiguriert');
|
||||
}
|
||||
this.logger?.info?.('Fuehre Agrarmonitor-Login durch');
|
||||
this.logLoginDetail('Login gestartet', {
|
||||
phase: 'login-start',
|
||||
baseUrl: this.baseUrl,
|
||||
username: this.options.username,
|
||||
loginStrategy: this.loginStrategy,
|
||||
autoRetry: this.autoRetry,
|
||||
cookies: this.formatCookies(this.snapshotCookies()),
|
||||
});
|
||||
if (this.loginStrategy === 'auto') {
|
||||
this.logger?.warn?.('loginStrategy "auto" ist veraltet; verwende Redirect-Login');
|
||||
}
|
||||
@@ -495,14 +503,27 @@ class AgrarmonitorConnector {
|
||||
}
|
||||
await this.options.cookieStore.save(this.cookieJar);
|
||||
this.logger?.info?.('Agrarmonitor-Login erfolgreich');
|
||||
this.logLoginDetail('Login abgeschlossen', {
|
||||
phase: 'login-done',
|
||||
cookies: this.formatCookies(this.snapshotCookies()),
|
||||
});
|
||||
}
|
||||
async performRedirectLogin() {
|
||||
const loginPageResponse = await this.http.get('/', this.loginRequestConfig());
|
||||
const loginPageResponse = await this.loggedRequest('Loginseite', 'GET', '/', () => this.http.get('/', this.loginRequestConfig()));
|
||||
const loginPageText = typeof loginPageResponse.data === 'string' ? loginPageResponse.data : '';
|
||||
if (!this.isLoginPageText(loginPageText)) {
|
||||
this.logLoginDetail('Keine Loginseite erhalten, Session ist bereits gueltig', {
|
||||
phase: 'login-skip',
|
||||
url: '/',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const nonce = this.extractNonce(loginPageText, 'input[name="nonce"]');
|
||||
this.logLoginDetail('Nonce aus Loginseite gelesen', {
|
||||
phase: 'nonce',
|
||||
url: '/',
|
||||
nonce: this.maskNonce(nonce),
|
||||
});
|
||||
const loginData = {
|
||||
username: this.options.username,
|
||||
passwort: this.options.password,
|
||||
@@ -516,7 +537,10 @@ class AgrarmonitorConnector {
|
||||
},
|
||||
_agrarmonitorLoginRequest: true,
|
||||
};
|
||||
const response = await this.http.post('/login/api/login.php', loginData, loginPostConfig);
|
||||
const response = await this.loggedRequest('Login-POST', 'POST', '/login/api/login.php', () => this.http.post('/login/api/login.php', loginData, loginPostConfig), {
|
||||
contentType: 'application/json',
|
||||
body: { ...loginData, passwort: '***', nonce: this.maskNonce(nonce) },
|
||||
});
|
||||
const responseText = typeof response.data === 'string' ? response.data : '';
|
||||
if (this.isLoginPageText(responseText)) {
|
||||
throw new Error('Agrarmonitor-Redirect-Login fehlgeschlagen');
|
||||
@@ -525,9 +549,25 @@ class AgrarmonitorConnector {
|
||||
async isSessionValid(options = {}) {
|
||||
try {
|
||||
const response = await this.http.get('/', options.skipAutoRetry ? this.loginRequestConfig() : undefined);
|
||||
return !this.isLoginRequiredResponse(response);
|
||||
const grund = this.loginRequiredReason(response);
|
||||
this.logLoginDetail(`Session-Pruefung: ${grund ? 'ungueltig' : 'gueltig'}`, {
|
||||
phase: 'session-check',
|
||||
url: '/',
|
||||
status: response.status,
|
||||
gueltig: grund === null,
|
||||
grund,
|
||||
cookies: this.formatCookies(this.snapshotCookies()),
|
||||
});
|
||||
return grund === null;
|
||||
}
|
||||
catch {
|
||||
catch (error) {
|
||||
this.logLoginDetail('Session-Pruefung fehlgeschlagen', {
|
||||
phase: 'session-check',
|
||||
url: '/',
|
||||
gueltig: false,
|
||||
grund: 'request-fehler',
|
||||
fehler: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -537,20 +577,38 @@ class AgrarmonitorConnector {
|
||||
};
|
||||
}
|
||||
isLoginRequiredResponse(response) {
|
||||
const responseUrl = this.getResponseUrl(response);
|
||||
return this.loginRequiredReason(response) !== null;
|
||||
}
|
||||
/**
|
||||
* Liefert das Kriterium, an dem eine abgelaufene Session erkannt wurde,
|
||||
* oder null fuer eine gueltige Session.
|
||||
*/
|
||||
loginRequiredReason(response) {
|
||||
const responseText = typeof response.data === 'string' ? response.data : '';
|
||||
return (response.status === 401 ||
|
||||
response.status === 403 ||
|
||||
this.isLoginPageText(responseText));
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
return `status-${response.status}`;
|
||||
}
|
||||
if (this.isLoginPageText(responseText)) {
|
||||
return 'login-markup';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async retryAfterLogin(config) {
|
||||
if (config._agrarmonitorRetry) {
|
||||
throw new Error('Agrarmonitor-Request nach erneutem Login weiterhin nicht autorisiert');
|
||||
}
|
||||
config._agrarmonitorRetry = true;
|
||||
const method = (config.method ?? 'get').toUpperCase();
|
||||
const url = config.url ?? '/';
|
||||
this.logger?.info?.('Agrarmonitor-Session abgelaufen, erneuter Login wird ausgefuehrt');
|
||||
this.logLoginDetail(`Session abgelaufen, wiederhole ${method} ${url} nach erneutem Login`, {
|
||||
phase: 'session-expired',
|
||||
method,
|
||||
url,
|
||||
cookies: this.formatCookies(this.snapshotCookies()),
|
||||
});
|
||||
await this.login();
|
||||
return this.http.request(config);
|
||||
return this.loggedRequest('Wiederholung nach Login', method, url, () => this.http.request(config), {}, 'retry');
|
||||
}
|
||||
createDateienLivesearchParams(suchstring) {
|
||||
return {
|
||||
@@ -731,6 +789,124 @@ class AgrarmonitorConnector {
|
||||
maskNonce(nonce) {
|
||||
return nonce.length <= 10 ? nonce : `${nonce.slice(0, 10)}...`;
|
||||
}
|
||||
get debugLogging() {
|
||||
return typeof this.logger?.debug === 'function';
|
||||
}
|
||||
/**
|
||||
* Detail-Logging des Login-Flows. Anders als logDebug faellt das bewusst
|
||||
* NICHT auf info zurueck - ohne debug-Logger bleiben die Details still.
|
||||
*/
|
||||
logLoginDetail(message, meta) {
|
||||
this.logger?.debug?.(message, meta);
|
||||
}
|
||||
maskValue(value) {
|
||||
if (!value) {
|
||||
return '(leer)';
|
||||
}
|
||||
return value.length <= 8 ? `\u2026(len=${value.length})` : `${value.slice(0, 4)}\u2026(len=${value.length})`;
|
||||
}
|
||||
/**
|
||||
* Liest die Cookies des Jars fuer eine URL aus. Liefert eine leere Liste,
|
||||
* solange kein debug-Logger haengt - dann wird gar nicht erst serialisiert.
|
||||
*/
|
||||
snapshotCookies(url = this.baseUrl) {
|
||||
if (!this.debugLogging) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
return this.cookieJar.getCookiesSync(url).map(cookie => ({
|
||||
name: cookie.key,
|
||||
value: cookie.value,
|
||||
domain: cookie.domain ?? null,
|
||||
path: cookie.path ?? null,
|
||||
expires: cookie.expires instanceof Date ? cookie.expires.toISOString() : 'Session',
|
||||
httpOnly: Boolean(cookie.httpOnly),
|
||||
secure: Boolean(cookie.secure),
|
||||
}));
|
||||
}
|
||||
catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Maskiert die Werte im rohen Set-Cookie-Header - sonst stuende die neue
|
||||
* Session-ID im Klartext im Log, obwohl der Jar-Snapshot sie maskiert.
|
||||
*/
|
||||
formatSetCookieHeader(header) {
|
||||
if (!Array.isArray(header)) {
|
||||
return null;
|
||||
}
|
||||
return header.map(entry => {
|
||||
const [pair, ...attributes] = String(entry).split(';');
|
||||
const separator = pair.indexOf('=');
|
||||
if (separator === -1) {
|
||||
return [pair, ...attributes].join(';');
|
||||
}
|
||||
const name = pair.slice(0, separator);
|
||||
const value = pair.slice(separator + 1);
|
||||
return [`${name}=${this.maskValue(value)}`, ...attributes].join(';');
|
||||
});
|
||||
}
|
||||
/** Wie snapshotCookies, aber mit maskierten Werten - nur das geht ins Log. */
|
||||
formatCookies(cookies) {
|
||||
return cookies.map(cookie => ({ ...cookie, value: this.maskValue(cookie.value) }));
|
||||
}
|
||||
diffCookies(before, after) {
|
||||
const beforeByName = new Map(before.map(cookie => [cookie.name, cookie.value]));
|
||||
const afterByName = new Map(after.map(cookie => [cookie.name, cookie.value]));
|
||||
return {
|
||||
added: [...afterByName.keys()].filter(name => !beforeByName.has(name)),
|
||||
changed: [...afterByName.entries()]
|
||||
.filter(([name, value]) => beforeByName.has(name) && beforeByName.get(name) !== value)
|
||||
.map(([name]) => name),
|
||||
removed: [...beforeByName.keys()].filter(name => !afterByName.has(name)),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Klammert einen Request mit je einem Cookie-Snapshot davor und danach.
|
||||
* Der Snapshot davor wird vor dem Absenden geloggt, damit er auch dann
|
||||
* im Log steht, wenn der Request haengt oder wirft.
|
||||
*/
|
||||
async loggedRequest(label, method, url, exec, details = {}, phase = 'request') {
|
||||
const before = this.snapshotCookies();
|
||||
this.logLoginDetail(`${label}: ${method} ${url}`, {
|
||||
phase,
|
||||
method,
|
||||
url,
|
||||
cookies: this.formatCookies(before),
|
||||
...details,
|
||||
});
|
||||
try {
|
||||
const response = await exec();
|
||||
const after = this.snapshotCookies();
|
||||
const responseText = typeof response.data === 'string' ? response.data : '';
|
||||
this.logLoginDetail(`${label}: ${method} ${url} -> ${response.status}`, {
|
||||
phase: 'response',
|
||||
method,
|
||||
url,
|
||||
status: response.status,
|
||||
responseUrl: this.getResponseUrl(response) || null,
|
||||
contentType: this.getHeader(response, 'content-type'),
|
||||
setCookie: this.formatSetCookieHeader(response.headers['set-cookie']),
|
||||
istLoginSeite: this.isLoginPageText(responseText),
|
||||
cookies: this.formatCookies(after),
|
||||
cookieChanges: this.diffCookies(before, after),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
catch (error) {
|
||||
const after = this.snapshotCookies();
|
||||
this.logLoginDetail(`${label}: ${method} ${url} -> Fehler`, {
|
||||
phase: 'error',
|
||||
method,
|
||||
url,
|
||||
fehler: error instanceof Error ? error.message : String(error),
|
||||
cookies: this.formatCookies(after),
|
||||
cookieChanges: this.diffCookies(before, after),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.AgrarmonitorConnector = AgrarmonitorConnector;
|
||||
//# sourceMappingURL=AgrarmonitorConnector.js.map
|
||||
Reference in New Issue
Block a user