server/maskLog.mjs

import isestr from 'wsemi/src/isestr.mjs'
import isarr from 'wsemi/src/isarr.mjs'


//maskLog: 權杖與網址寫入 console / srLog 前之遮罩(M 契約, spec/設計要點與取捨.md ADR-023)
//四 repo 一致(w-web-sso server/srLog.mjs 之 maskToken 與 src/maskUrl.mjs、w-web-api / w-web-task 之 maskTok), 以同一組測資守護(test/unit-maskLog.test.mjs)
//why: 原 console 站點原樣印出 token / req.query / authorization(Hapi 對重複之 query 參數給陣列, 型別檢查失敗之分支正是印出處), verifyConn 之 srLog 原樣記 referer


//safe: 露出字元之控制字元換成 '?', 避免請求端以 CR/LF 偽造 log 行
function safe(s) {
    return Array.from(s, (c) => {
        let x = c.charCodeAt(0)
        return (x < 32 || x === 127) ? '?' : c
    }).join('')
}


/**
 * 權杖遮罩
 * - 空值(undefined / null / '')回 ''
 * - 陣列逐元素遮罩
 * - 字串(長度 n)露出前後各 k=min(4, floor(n/8)) 字: k=0 只回 '(len=n)', 否則 '前k...後k(len=n)'; 36 字元之 session token 為前 4 後 4(與 w-web-sso 格式相同)
 * - 其他型別只回 '(typeof)', 絕不輸出原值
 *
 * @param {*} token 輸入權杖
 * @returns {String|Array} 回傳遮罩後字串, 輸入陣列時回傳陣列
 */
function maskTok(token) {
    if (isarr(token)) {
        return token.map(maskTok)
    }
    if (token === undefined || token === null || token === '') {
        return ''
    }
    if (!isestr(token)) {
        return `(${typeof token})`
    }
    let n = token.length
    let k = Math.min(4, Math.floor(n / 8))
    if (k === 0) {
        return `(len=${n})`
    }
    return `${safe(token.slice(0, k))}...${safe(token.slice(-k))}(len=${n})`
}


/**
 * 查詢參數遮罩: 淺拷貝, 鍵名小寫為 token 者以 maskTok 處理, 其餘原樣(供印出 req.query)
 * 非物件輸入依 maskTok 處理
 *
 * @param {*} q 輸入查詢參數物件(Hapi 之 req.query 為 null-prototype 物件)
 * @returns {Object|String|Array} 回傳遮罩後之淺拷貝
 */
function maskQuery(q) {
    if (q === null || typeof q !== 'object' || isarr(q)) {
        return maskTok(q)
    }
    let r = {}
    for (let k of Object.keys(q)) {
        r[k] = (k.toLowerCase() === 'token') ? maskTok(q[k]) : q[k]
    }
    return r
}


/**
 * 網址遮罩: http(s) 只留 origin + pathname, 捨棄 query / fragment / userinfo(其內常含權杖, 如 ?token=)
 * - 陣列逐元素處理; 空值回 ''; 其他非字串只回 '(typeof)'
 * - 非 http(s) 之網址(如 data:、javascript:)路徑即全部內容, 只回 protocol(同 w-web-sso src/maskUrl.mjs)
 * - 無法解析回 '(invalid-url)'
 *
 * @param {*} u 輸入網址
 * @returns {String|Array} 回傳可寫入 log 之字串, 輸入陣列時回傳陣列
 */
function maskUrl(u) {
    if (isarr(u)) {
        return u.map(maskUrl)
    }
    if (u === undefined || u === null || u === '') {
        return ''
    }
    if (!isestr(u)) {
        return `(${typeof u})`
    }
    let x = null
    try {
        x = new URL(u)
    }
    catch (err) {
        return '(invalid-url)'
    }
    if (x.protocol !== 'http:' && x.protocol !== 'https:') {
        return x.protocol
    }
    return `${x.origin}${x.pathname}`
}


export {
    maskTok,
    maskQuery,
    maskUrl
}