diff --git a/server/config/bbb.js b/server/config/bbb.js index 869db04..4851c90 100644 --- a/server/config/bbb.js +++ b/server/config/bbb.js @@ -18,19 +18,75 @@ function buildUrl(apiCall, params = {}) { async function apiCall(apiCallName, params = {}, xmlBody = null) { const url = buildUrl(apiCallName, params); + // Logging: compact key=value style, filter sensitive params + function formatUTC(d) { + const pad = n => String(n).padStart(2, '0'); + const Y = d.getUTCFullYear(); + const M = pad(d.getUTCMonth() + 1); + const D = pad(d.getUTCDate()); + const h = pad(d.getUTCHours()); + const m = pad(d.getUTCMinutes()); + const s = pad(d.getUTCSeconds()); + return `${Y}-${M}-${D} ${h}:${m}:${s} UTC`; + } + + const SENSITIVE_KEYS = [/pass/i, /pwd/i, /password/i, /token/i, /jwt/i, /secret/i, /authorization/i, /auth/i, /api[_-]?key/i]; + const isSensitive = key => SENSITIVE_KEYS.some(rx => rx.test(key)); + + function sanitizeParams(p) { + try { + const out = []; + for (const k of Object.keys(p || {})) { + if (k.toLowerCase() === 'checksum') continue; // never log checksum + if (isSensitive(k)) { + out.push(`${k}=[FILTERED]`); + } else { + let v = p[k]; + if (typeof v === 'string' && v.length > 100) v = v.slice(0, 100) + '...[truncated]'; + out.push(`${k}=${String(v)}`); + } + } + return out.join('&') || '-'; + } catch (e) { + return '-'; + } + } + + const start = Date.now(); try { const fetchOptions = xmlBody ? { method: 'POST', headers: { 'Content-Type': 'application/xml' }, body: xmlBody } : {}; const response = await fetch(url, fetchOptions); + const duration = Date.now() - start; const xml = await response.text(); const result = await xml2js.parseStringPromise(xml, { explicitArray: false, trim: true, }); + + // Compact log: time=... method=GET path=getMeetings format=xml status=200 duration=12.34 bbb_returncode=SUCCESS params=meetingID=123 + try { + const tokens = []; + tokens.push(`time=${formatUTC(new Date())}`); + tokens.push(`method=${xmlBody ? 'POST' : 'GET'}`); + tokens.push(`path=${apiCallName}`); + tokens.push(`format=xml`); + tokens.push(`status=${response.status}`); + tokens.push(`duration=${(duration).toFixed(2)}`); + const bbbCode = result && result.response && result.response.returncode ? result.response.returncode : '-'; + tokens.push(`bbb_returncode=${bbbCode}`); + const safeParams = sanitizeParams(params); + tokens.push(`params=${safeParams}`); + console.info(tokens.join(' ')); + } catch (e) { + // ignore logging errors + } + return result.response; } catch (error) { - console.error(`BBB API error (${apiCallName}):`, error.message); + const duration = Date.now() - start; + console.error(`BBB API error (${apiCallName}) status=error duration=${(duration).toFixed(2)} err=${error.message}`); throw error; } } diff --git a/server/middleware/logging.js b/server/middleware/logging.js index b13ef17..1cf8a05 100644 --- a/server/middleware/logging.js +++ b/server/middleware/logging.js @@ -43,6 +43,17 @@ function filterHeaders(headers) { return out; } +function formatUTC(d) { + const pad = n => String(n).padStart(2, '0'); + const Y = d.getUTCFullYear(); + const M = pad(d.getUTCMonth() + 1); + const D = pad(d.getUTCDate()); + const h = pad(d.getUTCHours()); + const m = pad(d.getUTCMinutes()); + const s = pad(d.getUTCSeconds()); + return `${Y}-${M}-${D} ${h}:${m}:${s} UTC`; +} + export default function requestResponseLogger(req, res, next) { try { const start = Date.now(); @@ -73,51 +84,28 @@ export default function requestResponseLogger(req, res, next) { res.on('finish', () => { try { - const duration = Date.now() - start; - const resHeaders = filterHeaders(res.getHeaders ? res.getHeaders() : {}); - - let loggedResponseBody = null; + const duration = Date.now() - start; // ms const resContentType = (res.getHeader && (res.getHeader('content-type') || '')).toString().toLowerCase(); - if (responseBody === undefined) { - loggedResponseBody = '[none-captured]'; - } else if (resContentType.includes('application/json')) { - try { - const parsed = typeof responseBody === 'string' ? JSON.parse(responseBody) : responseBody; - loggedResponseBody = filterValue(null, parsed); - } catch (e) { - loggedResponseBody = typeof responseBody === 'string' ? (responseBody.length > 1000 ? responseBody.slice(0, 1000) + '...[truncated]' : responseBody) : util.inspect(responseBody); - } - } else if (resContentType.includes('text/') || resContentType.includes('application/xml')) { - const asStr = typeof responseBody === 'string' ? responseBody : util.inspect(responseBody); - loggedResponseBody = asStr.length > 1000 ? asStr.slice(0, 1000) + '...[truncated]' : asStr; - } else { - loggedResponseBody = '[not-logged-due-to-content-type]'; - } - const log = { - time: new Date().toISOString(), - ip, - method, - url: originalUrl, - status: res.statusCode, - duration_ms: duration, - request: { - headers: reqHeaders, - query: filterValue(null, req.query || {}), - body: reqBody, - }, - response: { - headers: resHeaders, - body: loggedResponseBody, - }, - }; + // Compact key=value log (no bodies, sensitive data filtered) + const tokens = []; + tokens.push(`time=${formatUTC(new Date())}`); + tokens.push(`method=${method}`); + tokens.push(`path=${originalUrl.replace(/\s/g, '%20')}`); + const fmt = resContentType.includes('json') ? 'json' : (resContentType.includes('html') ? 'html' : 'other'); + tokens.push(`format=${fmt}`); + tokens.push(`status=${res.statusCode}`); + tokens.push(`duration=${(duration).toFixed(2)}`); - // Output as single-line JSON for log aggregation + // Optional: content-length if available try { - console.info(JSON.stringify(log)); + const cl = res.getHeader && (res.getHeader('content-length') || res.getHeader('Content-Length')); + if (cl) tokens.push(`length=${String(cl)}`); } catch (e) { - console.info('LOG_ERROR', util.inspect(log)); + // ignore } + + console.info(tokens.join(' ')); } catch (e) { console.error('RequestLogger error:', e); }