RSS Git Download  Clone
Raw Blame History 4kB 125 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 ''
    }
}

export {
    parseBoolean,
    resolveConfiguredHttpAuth,
    verifyAuthorizationHeader,
    readPasswordHashFromFile,
}