RSS Git Download  Clone
Raw Blame History 5kB 176 lines
import fs from 'fs'
import crypto from 'crypto'
import bcrypt from 'bcryptjs'

const parseBoolean = (value) => {
    if (typeof value === 'boolean') {
        return value
    }
    if (typeof value !== 'string') {
        return undefined
    }
    const normalized = value.trim().toLowerCase()
    if (['1', 'true', 'yes', 'on'].includes(normalized)) {
        return true
    }
    if (['0', 'false', 'no', 'off'].includes(normalized)) {
        return false
    }
    return undefined
}

const resolveConfiguredHttpAuth = () => {
    const cfg = p3xrs && p3xrs.cfg && typeof p3xrs.cfg === 'object' ? p3xrs.cfg : {}
    const fromServer = cfg.server && typeof cfg.server.httpAuth === 'object' ? cfg.server.httpAuth : {}
    const fromRoot = cfg.httpAuth && typeof cfg.httpAuth === 'object' ? cfg.httpAuth : {}
    const merged = Object.assign({}, fromServer, fromRoot)

    const username = typeof merged.username === 'string' && merged.username.length > 0 ? merged.username : 'admin'
    const password = typeof merged.password === 'string' ? merged.password : ''
    const passwordHash = typeof merged.passwordHash === 'string' ? merged.passwordHash.trim() : ''
    const enabledRaw = parseBoolean(merged.enabled)
    const hasSecret = password.length > 0 || passwordHash.length > 0
    const enabled = enabledRaw === undefined ? hasSecret : enabledRaw

    return {
        enabled,
        username,
        password,
        passwordHash,
    }
}

const safeCompare = (a, b) => {
    const aBuffer = Buffer.from(typeof a === 'string' ? a : '', 'utf8')
    const bBuffer = Buffer.from(typeof b === 'string' ? b : '', 'utf8')
    if (aBuffer.length !== bBuffer.length) {
        return false
    }
    return crypto.timingSafeEqual(aBuffer, bBuffer)
}

const parseBasicAuthorizationHeader = (headerValue) => {
    if (typeof headerValue !== 'string' || headerValue.length === 0) {
        return null
    }
    const parts = headerValue.split(' ')
    if (parts.length !== 2 || parts[0].toLowerCase() !== 'basic') {
        return null
    }
    const decoded = Buffer.from(parts[1], 'base64').toString('utf8')
    const colonIndex = decoded.indexOf(':')
    if (colonIndex === -1) {
        return null
    }
    return {
        username: decoded.slice(0, colonIndex),
        password: decoded.slice(colonIndex + 1),
    }
}

const verifyCredentials = ({ username, password }) => {
    const settings = resolveConfiguredHttpAuth()
    if (!settings.enabled) {
        return true
    }
    if (!safeCompare(username, settings.username)) {
        return false
    }
    if (settings.passwordHash.length > 0) {
        try {
            return bcrypt.compareSync(password, settings.passwordHash)
        } catch (e) {
            return false
        }
    }
    if (settings.password.length > 0) {
        return safeCompare(password, settings.password)
    }
    return false
}

const verifyAuthorizationHeader = (headerValue) => {
    const settings = resolveConfiguredHttpAuth()
    if (!settings.enabled) {
        return true
    }
    const parsed = parseBasicAuthorizationHeader(headerValue)
    if (!parsed) {
        return false
    }
    return verifyCredentials(parsed)
}

const readPasswordHashFromFile = (filename) => {
    if (typeof filename !== 'string' || filename.trim() === '') {
        return ''
    }
    const resolved = filename.trim()
    if (!fs.existsSync(resolved)) {
        return ''
    }
    try {
        return fs.readFileSync(resolved, 'utf8').trim()
    } catch (e) {
        return ''
    }
}

// JWT token support (HS256, no external dependency)
let _jwtSecret = null

const getJwtSecret = () => {
    if (_jwtSecret) return _jwtSecret
    const settings = resolveConfiguredHttpAuth()
    const source = settings.passwordHash || settings.password || crypto.randomBytes(32).toString('hex')
    _jwtSecret = crypto.createHash('sha256').update('p3xrs-jwt-' + source).digest()
    return _jwtSecret
}

const createAuthToken = (username) => {
    const secret = getJwtSecret()
    const payload = {
        sub: username,
        iat: Math.floor(Date.now() / 1000),
        exp: Math.floor(Date.now() / 1000) + 86400, // 24 hours
    }
    const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url')
    const body = Buffer.from(JSON.stringify(payload)).toString('base64url')
    const signature = crypto.createHmac('sha256', secret).update(`${header}.${body}`).digest('base64url')
    return `${header}.${body}.${signature}`
}

const verifyAuthToken = (token) => {
    if (typeof token !== 'string' || token.length === 0) return null
    try {
        const secret = getJwtSecret()
        const parts = token.split('.')
        if (parts.length !== 3) return null
        const [header, body, signature] = parts
        const expected = crypto.createHmac('sha256', secret).update(`${header}.${body}`).digest('base64url')
        const sigBuf = Buffer.from(signature, 'utf8')
        const expBuf = Buffer.from(expected, 'utf8')
        if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) return null
        const payload = JSON.parse(Buffer.from(body, 'base64url').toString('utf8'))
        if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) return null
        return payload
    } catch (e) {
        return null
    }
}

const resetJwtSecret = () => {
    _jwtSecret = null
}

export {
    parseBoolean,
    resolveConfiguredHttpAuth,
    verifyCredentials,
    verifyAuthorizationHeader,
    readPasswordHashFromFile,
    createAuthToken,
    verifyAuthToken,
    resetJwtSecret,
}