import logger from "./logger" import { networkInterfaces } from 'node:os' import mime from "./mime" import { page_layout } from "./templates" export const is_node_version = (op: '=' | '<=' | '>=', version: string) => { const nodeVersion = process.version const matches = nodeVersion.match(/\d+/g) const vMatches = version.match(/\d+/g) if (!matches || !vMatches) { return false } const [major, minor, patch] = matches.map(Number) const [vMajor, vMinor, vPatch] = vMatches.map(Number) switch (op) { case '=': return process.version === version case '<=': return major < vMajor || (major === vMajor && minor <= vMinor) || (major === vMajor && minor === vMinor && patch <= vPatch) case '>=': return major > vMajor || (major === vMajor && minor >= vMinor) || (major === vMajor && minor === vMinor && patch >= vPatch) } } export const fixFunctionName = (name: string) => { return name.replace(/[^a-zA-Z0-9_]/g, '_') } export const REG_FILENAME = /[^\\/,\s\t\n]+/g export const pathname_arr = (str = ''): string[] => (str.split(/[#?]+/)[0].replace(/^\.+\//, '').match(REG_FILENAME) || []) // 标准化路径, 避免路径遍历漏洞 只能处理相对路径 export const pathname_fixer = (str = '') => pathname_arr(str).join('/') // 获取路径的目录名,忽略 ../ 等上级目录 export const pathname_dirname = (str = '') => (str.match(REG_FILENAME) || []).slice(0, -1).join('/') export const createSessionId = () => 'xxxx-xxxx-xxxx-xxxx'.replace(/xxxx/g, () => Math.floor((1 << 16) + Math.random() * (1 << 24)).toString(16).substring(0, 4)) export const minimatch = (str = '', pattern = '') => { const reg = new RegExp(pattern.replace(/\./g, '\\.').replace(/\*/g, '.*').replace(/,/g, '|')) return reg.test(str) } // 为所有异步操作添加超时 export const withTimeout = (promise: Promise, timeout: number, errorMessage: string = 'timeout'): Promise => { return Promise.race([ promise, new Promise((_, reject) => setTimeout(() => reject(new Error(errorMessage)), timeout) ) ]) } // 安全的 JSON 解析 export const safeJsonParse = (str: string, reviver?: (key: string, value: any) => any, MAX_JSON_DEPTH = 32) => { let depth = 0 let inString = false let escapeNext = false for (let i = 0; i < str.length; i++) { if (escapeNext) { escapeNext = false continue } if (str[i] === '\\') { escapeNext = true continue } if (str[i] === '"') { inString = !inString continue } if (!inString) { if (str[i] === '{' || str[i] === '[') depth++ if (str[i] === '}' || str[i] === ']') depth-- if (depth > MAX_JSON_DEPTH) throw new Error('JSON depth exceeded') } } try { return JSON.parse(str, reviver) } catch (e) { logger.error(`[safeJsonParse] error:`, e) return null } } export const getMimeType = (pathname: string) => { const suffix = (pathname || '').split('.').pop() || '' return mime.getType(suffix) || 'application/octet-stream' } export const isText = (pathname: string) => { const type = getMimeType(pathname) return /\b(html?|txt|javascript|json)\b/.test(type || 'exe') } export const decode = (str: string) => { try { return decodeURIComponent(str) } catch (e) { logger.warn(e) return str } } export const toBuffer = function (arrayBuffer: ArrayBuffer) { const buffer = Buffer.alloc(arrayBuffer.byteLength); const arrayBufferView = new Uint8Array(arrayBuffer); for (let i = 0; i < arrayBufferView.length; i++) { buffer[i] = arrayBufferView[i]; } return buffer } type AdapterInfo = { family: string | number internal: boolean address?: string } export const getServerIPs = () => { const networks = networkInterfaces() const adapters = Object.values(networks).reduce((acc, list) => { if (Array.isArray(list)) { acc.push(...list.filter(Boolean) as AdapterInfo[]) } return acc }, []) const ips = adapters.filter((info) => { const family = typeof info.family === 'string' ? info.family : String(info.family) return family === 'IPv4' && !info.internal && Boolean(info.address) }).map((info) => info.address) return ['127.0.0.1', ...ips] } export const queryparams = (search: string) => { const searchParams = new URLSearchParams(search) const params: Record = {} searchParams.forEach((v, k) => { if (params[k]) { params[k] = ([] as string[]).concat(params[k]).concat(v) } else { params[k] = v } }) return params } export const get = function loopGet (obj: any, path: string | string[]): any { const [key, ...rest] = path.toString().match(REG_FILENAME) || [] if (!key || !obj) { return obj } if (rest.length === 0) { return obj[key] } return loopGet(obj[key], rest) } export const set = function loopSet (obj: any, path: string | string[], value: any): any { const [key, ...rest] = path.toString().match(REG_FILENAME) || [] if (!key) return if (rest.length === 0) { Object.assign(obj, { [key]: value }) } else { if (!obj[key]) { obj[key] = {} } loopSet(obj[key], rest, value) } } export const isPlainObject = function (value: any) { if (!value || typeof value != 'object') { return false } return Object.prototype.toString.call(value) === '[object Object]' } /** 简单字符串模板,类似 handlebars */ export const template = function template (tpl: string, data: any, toBlank = true, index?: number): string { return tpl .replace(/{{!--[\s\S]*?--}}/g, '') // 删除注释 .replace(/\{\{#?(\w+)\s+(\w+)[^{}]*\}\}([\s\S\t\r\n]*?)\{\{\/\1\}\}/g, function (_: any, fn: any, item_key: any, line: any) { const items = data[item_key] const _placeholder = toBlank ? '' : _ switch (fn) { case 'each': return items ? items.map((item: any, index: number) => template(line, item, toBlank, index)).join('') : _placeholder case 'if': return items ? template(line, items) : _placeholder default: return template(line, items) } }) .replace(/\{\{([@\$\.\w]+)\}\}/g, (__, key) => { const _placeholder = toBlank ? '' : __ switch (key) { case '@': return typeof data !== 'undefined' ? data : _placeholder; case '@index': return index; default: return typeof data[key] !== 'undefined' ? data[key] : _placeholder } }) } export const renderHTML = (body: string, data: any) => { if (!isPlainObject(data)) { return body } return template(page_layout, { title: data.title || 'F2E Page', body: template(body, data) }) }