From b8e08666a60893aa9127f42e0c44ee1d34ca047a Mon Sep 17 00:00:00 2001 From: chaoszhu Date: Wed, 10 Jul 2024 13:21:47 +0800 Subject: [PATCH] :sparkles: update db --- .gitignore | 6 +- CHANGELOG.md | 10 + Q&A.md | 18 ++ README.md | 8 + package.json | 2 +- server/.gitignore | 7 - server/app/config/index.js | 16 +- server/app/controller/group.js | 41 ++-- server/app/controller/host.js | 47 ++-- server/app/controller/notify.js | 56 ++--- server/app/controller/ssh.js | 22 +- server/app/controller/user.js | 23 +- server/app/db.js | 154 ++++++++++++ server/app/{storage => db}/README.md | 78 +++--- server/app/init.js | 27 +-- server/app/main.js | 19 +- server/app/middlewares/auth.js | 4 +- server/app/middlewares/compress.js | 1 + server/app/router/routes.js | 8 +- server/app/schedule/expired-notify.js | 2 +- server/app/schedule/offline-inspect.js | 5 +- server/app/socket/clients.js | 18 +- server/app/socket/host-status.js | 6 +- server/app/socket/monitor.js | 2 +- server/app/socket/sftp.js | 14 +- server/app/socket/terminal.js | 31 ++- server/app/storage/email.json | 36 --- server/app/storage/group.json | 7 - server/app/storage/host-list.json | 1 - server/app/storage/key.json | 6 - server/app/storage/notify.json | 22 -- server/app/storage/ssh-record.json | 1 - server/app/utils/db-class.js | 68 ++++++ server/app/utils/email.js | 8 +- server/app/utils/encrypt.js | 22 +- server/app/utils/index.js | 15 +- server/app/utils/storage.js | 313 +++++++++++++++++-------- server/app/utils/tools.js | 80 ++++++- server/app/utils/verify-auth.js | 22 +- server/package.json | 1 + yarn.lock | 189 ++++++++++++++- 41 files changed, 990 insertions(+), 426 deletions(-) delete mode 100644 server/.gitignore create mode 100644 server/app/db.js rename server/app/{storage => db}/README.md (74%) delete mode 100644 server/app/storage/email.json delete mode 100644 server/app/storage/group.json delete mode 100644 server/app/storage/host-list.json delete mode 100644 server/app/storage/key.json delete mode 100644 server/app/storage/notify.json delete mode 100644 server/app/storage/ssh-record.json create mode 100644 server/app/utils/db-class.js diff --git a/.gitignore b/.gitignore index 172ad6f..046869f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,8 @@ dist easynode-server.zip server/app/static/upload/* server/app/socket/temp/* -server/app/logs/* \ No newline at end of file +app/socket/.sftp-cache/* +server/app/logs/* +server/app/db/* +!server/app/db/README.md +plan.md diff --git a/CHANGELOG.md b/CHANGELOG.md index fbb2b1b..d5a8708 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## [1.3.0](https://github.com/chaos-zhu/easynode/releases) (2024-07-10) + +### Features + +* 重构文件储存方式 ✔ +* 修复不同ssh密钥算法登录失败的bug ✔ +* 移除上一次IP登录校验的判断 ✔ +* 前端工程迁移至项目根目录 ✔ +* 添加ssh密钥保存至本地功能 ✔ + ## [1.2.1](https://github.com/chaos-zhu/easynode/releases) (2022-12-12) ### Features diff --git a/Q&A.md b/Q&A.md index d5d0d48..4e03d54 100644 --- a/Q&A.md +++ b/Q&A.md @@ -1,5 +1,23 @@ # Q&A +## ssh连接失败 + +首先确定用户名/密码/密钥没错,接着排查服务端ssh登录日志,例如Debian12 `journalctl -u ssh -f` + +如果出现类似以下日志: +```shell +Jul 10 12:29:11 iZ2ze5f4ne9xf8n3h5Z sshd[8020]: userauth_pubkey: signature algorithm ssh-rsa not in PubkeyAcceptedAlgorithms [preauth] +``` + +说明客户端 `ssh-rsa` 签名算法不在 `PubkeyAcceptedAlgorithms` 列表中,目标服务器不接受 ssh-rsa 签名算法的公钥认证。 + +**解决: ** +编辑 /etc/ssh/sshd_config 文件,添加或修改以下配置 +```shell +PubkeyAcceptedAlgorithms +ssh-rsa +``` +重新启动 SSH 服务: `sudo systemctl restart sshd` + ## CentOS7/8启动服务失败 > 先关闭SELinux diff --git a/README.md b/README.md index 55858e2..0da1fd5 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ - [客户端](#客户端) - [版本日志](#版本日志) - [安全与说明](#安全与说明) + - [开发](#开发) - [Q&A](#qa) - [感谢Star](#感谢star) - [License](#license) @@ -160,6 +161,13 @@ wget -qO- --no-check-certificate https://ghproxy.com/https://raw.githubuserconte > webssh功能需要的密钥信息全部保存在服务端服务器的`app\storage\ssh-record.json`中. 在保存ssh密钥信息到服务器储存与传输过程皆已加密,`不过最好还是套https使用` +## 开发 + +1. 拉取代码,准备nodejs环境>=16 +2. cd到项目根目录,yarn执行安装依赖 +3. cd web 启动前端 npm run dev +4. cd server 启动服务端 npm run local + ## Q&A - [Q&A](./Q%26A.md) diff --git a/package.json b/package.json index 4343a76..073e6eb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "easynode", "version": "1.0.0", - "description": "easy to manage the server", + "description": "web ssh", "private": true, "workspaces": ["server", "client"], "repository": { diff --git a/server/.gitignore b/server/.gitignore deleted file mode 100644 index 0a212e8..0000000 --- a/server/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -node_modules -app/static/upload/* -app/socket/temp/* -app/socket/.sftp-cache/* -app/logs/* -!.gitkeep -dist \ No newline at end of file diff --git a/server/app/config/index.js b/server/app/config/index.js index cb63a64..81e03da 100644 --- a/server/app/config/index.js +++ b/server/app/config/index.js @@ -2,19 +2,19 @@ const path = require('path') module.exports = { httpPort: 8082, - clientPort: 22022, // 勿更改 + clientPort: 22022, // 暂不支持更改 uploadDir: path.join(process.cwd(),'app/static/upload'), staticDir: path.join(process.cwd(),'app/static'), sftpCacheDir: path.join(process.cwd(),'app/socket/sftp-cache'), - sshRecordPath: path.join(process.cwd(),'app/storage/ssh-record.json'), - keyPath: path.join(process.cwd(),'app/storage/key.json'), - hostListPath: path.join(process.cwd(),'app/storage/host-list.json'), - emailPath: path.join(process.cwd(),'app/storage/email.json'), - notifyPath: path.join(process.cwd(),'app/storage/notify.json'), - groupPath: path.join(process.cwd(),'app/storage/group.json'), + sshRecordDBPath: path.join(process.cwd(),'app/db/ssh-record.db'), + keyDBPath: path.join(process.cwd(),'app/db/key.db'), + hostListDBPath: path.join(process.cwd(),'app/db/host-list.db'), + notifyConfDBPath: path.join(process.cwd(),'app/db/notify.db'), + groupConfDBPath: path.join(process.cwd(),'app/db/group.db'), + emailNotifyDBPath: path.join(process.cwd(),'app/db/email.db'), apiPrefix: '/api/v1', logConfig: { outDir: path.join(process.cwd(),'./app/logs'), - recordLog: false // 是否记录日志 + recordLog: true // 是否记录日志 } } diff --git a/server/app/controller/group.js b/server/app/controller/group.js index 1296575..7ec32ab 100644 --- a/server/app/controller/group.js +++ b/server/app/controller/group.js @@ -1,52 +1,51 @@ -const { readGroupList, writeGroupList, readHostList, writeHostList,randomStr } = require('../utils') +const { readGroupList, writeGroupList, readHostList, writeHostList, randomStr } = require('../utils') -function getGroupList({ res }) { - const data = readGroupList() +async function getGroupList({ res }) { + const data = await readGroupList() + data?.sort((a, b) => a.index - b.index) res.success({ data }) } const addGroupList = async ({ res, request }) => { let { body: { name, index } } = request - if(!name) return res.fail({ data: false, msg: '参数错误' }) - let groupList = readGroupList() + if (!name) return res.fail({ data: false, msg: '参数错误' }) + let groupList = await readGroupList() let group = { id: randomStr(), name, index } groupList.push(group) - groupList.sort((a, b) => a.index - b.index) - writeGroupList(groupList) + await writeGroupList(groupList) res.success({ data: '新增成功' }) } const updateGroupList = async ({ res, request }) => { let { params: { id } } = request let { body: { name, index } } = request - if(!id || !name) return res.fail({ data: false, msg: '参数错误' }) - let groupList = readGroupList() + if (!id || !name) return res.fail({ data: false, msg: '参数错误' }) + let groupList = await readGroupList() let idx = groupList.findIndex(item => item.id === id) - let group = { id, name, index } - if(idx === -1) return res.fail({ data: false, msg: `分组ID${ id }不存在` }) + let group = { id, name, index: Number(index) || 0 } + if (idx === -1) return res.fail({ data: false, msg: `分组ID${id}不存在` }) groupList.splice(idx, 1, group) - groupList.sort((a, b) => a.index - b.index) - writeGroupList(groupList) + await writeGroupList(groupList) res.success({ data: '修改成功' }) } const removeGroup = async ({ res, request }) => { let { params: { id } } = request - if(id ==='default') return res.fail({ data: false, msg: '保留分组, 禁止删除' }) - let groupList = readGroupList() + if (id === 'default') return res.fail({ data: false, msg: '保留分组, 禁止删除' }) + let groupList = await readGroupList() let idx = groupList.findIndex(item => item.id === id) - if(idx === -1) return res.fail({ msg: '分组不存在' }) + if (idx === -1) return res.fail({ msg: '分组不存在' }) // 移除分组将所有该分组下host分配到default中去 - let hostList = readHostList() - hostList = hostList.map((item) => { - if(item.group === groupList[idx].id) item.group = 'default' + let hostList = await readHostList() + hostList = hostList?.map((item) => { + if (item.group === groupList[idx].id) item.group = 'default' return item }) - writeHostList(hostList) + await writeHostList(hostList) groupList.splice(idx, 1) - writeGroupList(groupList) + await writeGroupList(groupList) res.success({ data: '移除成功' }) } diff --git a/server/app/controller/host.js b/server/app/controller/host.js index 39a0b19..32a2736 100644 --- a/server/app/controller/host.js +++ b/server/app/controller/host.js @@ -1,58 +1,61 @@ const { readHostList, writeHostList, readSSHRecord, writeSSHRecord } = require('../utils') -function getHostList({ res }) { - const data = readHostList() +async function getHostList({ res }) { + console.log('get - host - list') + const data = await readHostList() res.success({ data }) } -function saveHost({ res, request }) { +async function saveHost({ res, request }) { let { body: { host: newHost, name, expired, expiredNotify, group, consoleUrl, remark } } = request - if(!newHost || !name) return res.fail({ msg: 'missing params: name or host' }) - let hostList = readHostList() - if(hostList.some(({ host }) => host === newHost)) return res.fail({ msg: `主机${ newHost }已存在` }) + console.log(request) + if (!newHost || !name) return res.fail({ msg: 'missing params: name or host' }) + let hostList = await readHostList() + if (hostList?.some(({ host }) => host === newHost)) return res.fail({ msg: `主机${newHost}已存在` }) + if (!Array.isArray(hostList)) hostList = [] hostList.push({ host: newHost, name, expired, expiredNotify, group, consoleUrl, remark }) - writeHostList(hostList) + await writeHostList(hostList) res.success() } -function updateHost({ res, request }) { +async function updateHost({ res, request }) { let { body: { host: newHost, name: newName, oldHost, expired, expiredNotify, group, consoleUrl, remark } } = request - if(!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' }) - let hostList = readHostList() - if(!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `主机${ newHost }不存在` }) + if (!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' }) + let hostList = await readHostList() + if (!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `主机${newHost}不存在` }) let targetIdx = hostList.findIndex(({ host }) => host === oldHost) hostList.splice(targetIdx, 1, { name: newName, host: newHost, expired, expiredNotify, group, consoleUrl, remark }) writeHostList(hostList) res.success() } -function removeHost({ res, request }) { +async function removeHost({ res, request }) { let { body: { host } } = request - let hostList = readHostList() + let hostList = await readHostList() let hostIdx = hostList.findIndex(item => item.host === host) - if(hostIdx === -1) return res.fail({ msg: `${ host }不存在` }) + if (hostIdx === -1) return res.fail({ msg: `${host}不存在` }) hostList.splice(hostIdx, 1) writeHostList(hostList) // 查询是否存在ssh记录 - let sshRecord = readSSHRecord() + let sshRecord = await readSSHRecord() let sshIdx = sshRecord.findIndex(item => item.host === host) let flag = sshIdx !== -1 - if(flag) sshRecord.splice(sshIdx, 1) + if (flag) sshRecord.splice(sshIdx, 1) writeSSHRecord(sshRecord) - res.success({ data: `${ host }已移除, ${ flag ? '并移除ssh记录' : '' }` }) + res.success({ data: `${host}已移除, ${flag ? '并移除ssh记录' : ''}` }) } -function updateHostSort({ res, request }) { +async function updateHostSort({ res, request }) { let { body: { list } } = request - if(!list) return res.fail({ msg: '参数错误' }) - let hostList = readHostList() - if(hostList.length !== list.length) return res.fail({ msg: '失败: host数量不匹配' }) + if (!list) return res.fail({ msg: '参数错误' }) + let hostList = await readHostList() + if (hostList.length !== list.length) return res.fail({ msg: '失败: host数量不匹配' }) let sortResult = [] for (let i = 0; i < list.length; i++) { const curHost = list[i] let temp = hostList.find(({ host }) => curHost.host === host) - if(!temp) return res.fail({ msg: `查找失败: ${ curHost.name }` }) + if (!temp) return res.fail({ msg: `查找失败: ${curHost.name}` }) sortResult.push(temp) } writeHostList(sortResult) diff --git a/server/app/controller/notify.js b/server/app/controller/notify.js index bf65957..90b3612 100644 --- a/server/app/controller/notify.js +++ b/server/app/controller/notify.js @@ -7,14 +7,14 @@ const { writeNotifyList } = require('../utils') const commonTemp = require('../template/commonTemp') -function getSupportEmailList({ res }) { - const data = readSupportEmailList() +async function getSupportEmailList({ res }) { + const data = await readSupportEmailList() res.success({ data }) } -function getUserEmailList({ res }) { - const userEmailList = readUserEmailList().map(({ target, auth: { user } }) => ({ target, user })) - const supportEmailList = readSupportEmailList() +async function getUserEmailList({ res }) { + const userEmailList = (await readUserEmailList()).map(({ target, auth: { user } }) => ({ target, user })) + const supportEmailList = await readSupportEmailList() const data = userEmailList.map(({ target: userTarget, user: email }) => { let name = supportEmailList.find(({ target: supportTarget }) => supportTarget === userTarget).name return { name, email } @@ -24,57 +24,57 @@ function getUserEmailList({ res }) { async function pushEmail({ res, request }) { let { body: { toEmail, isTest } } = request - if(!isTest) return res.fail({ msg: '此接口暂时只做测试邮件使用, 需传递参数isTest: true' }) + if (!isTest) return res.fail({ msg: '此接口暂时只做测试邮件使用, 需传递参数isTest: true' }) consola.info('发送测试邮件:', toEmail) let { code, msg } = await emailTransporter({ toEmail, title: '测试邮件', html: commonTemp('邮件通知测试邮件') }) msg = msg && msg.message || msg - if(code === 0) return res.success({ msg }) + if (code === 0) return res.success({ msg }) return res.fail({ msg }) } -function updateUserEmailList({ res, request }) { +async function updateUserEmailList({ res, request }) { let { body: { target, auth } } = request - const supportList = readSupportEmailList() + const supportList = await readSupportEmailList() let flag = supportList.some((item) => item.target === target) - if(!flag) return res.fail({ msg: `不支持的邮箱类型:${ target }` }) - if(!auth.user || !auth.pass) return res.fail({ msg: 'missing params: auth.' }) + if (!flag) return res.fail({ msg: `不支持的邮箱类型:${ target }` }) + if (!auth.user || !auth.pass) return res.fail({ msg: 'missing params: auth.' }) let newUserEmail = { target, auth } - let userEmailList = readUserEmailList() + let userEmailList = await readUserEmailList() let idx = userEmailList.findIndex(({ auth: { user } }) => auth.user === user) - if(idx !== -1) userEmailList.splice(idx, 1, newUserEmail) + if (idx !== -1) userEmailList.splice(idx, 1, newUserEmail) else userEmailList.unshift(newUserEmail) - const { code, msg } = writeUserEmailList(userEmailList) - if(code === 0) return res.success() + const { code, msg } = await writeUserEmailList(userEmailList) + if (code === 0) return res.success() return res.fail({ msg }) } -function removeUserEmail({ res, request }) { +async function removeUserEmail({ res, request }) { let { params: { email } } = request - const userEmailList = readUserEmailList() + const userEmailList = await readUserEmailList() let idx = userEmailList.findIndex(({ auth: { user } }) => user === email) - if(idx === -1) return res.fail({ msg: `删除失败, 不存在该邮箱:${ email }` }) + if (idx === -1) return res.fail({ msg: `删除失败, 不存在该邮箱:${ email }` }) userEmailList.splice(idx, 1) - const { code, msg } = writeUserEmailList(userEmailList) - if(code === 0) return res.success() + const { code, msg } = await writeUserEmailList(userEmailList) + if (code === 0) return res.success() return res.fail({ msg }) } -function getNotifyList({ res }) { - const data = readNotifyList() +async function getNotifyList({ res }) { + const data = await readNotifyList() res.success({ data }) } -function updateNotifyList({ res, request }) { +async function updateNotifyList({ res, request }) { let { body: { type, sw } } = request - if(!([true, false].includes(sw))) return res.fail({ msg: `Error type for sw:${ sw }, must be Boolean` }) - const notifyList = readNotifyList() + if (!([true, false].includes(sw))) return res.fail({ msg: `Error type for sw:${ sw }, must be Boolean` }) + const notifyList = await readNotifyList() let target = notifyList.find((item) => item.type === type) - if(!target) return res.fail({ msg: `更新失败, 不存在该通知类型:${ type }` }) + if (!target) return res.fail({ msg: `更新失败, 不存在该通知类型:${ type }` }) target.sw = sw - console.log(notifyList) - writeNotifyList(notifyList) + // console.log(notifyList) + await writeNotifyList(notifyList) res.success() } diff --git a/server/app/controller/ssh.js b/server/app/controller/ssh.js index 5554c3f..d575c8a 100644 --- a/server/app/controller/ssh.js +++ b/server/app/controller/ssh.js @@ -1,47 +1,47 @@ -const { readSSHRecord, writeSSHRecord, AESEncrypt } = require('../utils') +const { readSSHRecord, writeSSHRecord, AESEncryptSync } = require('../utils') const updateSSH = async ({ res, request }) => { let { body: { host, port, username, type, password, privateKey, randomKey, command } } = request let record = { host, port, username, type, password, privateKey, randomKey, command } if(!host || !port || !username || !type || !randomKey) return res.fail({ data: false, msg: '参数错误' }) // 再做一次对称加密(方便ssh连接时解密) - record.randomKey = AESEncrypt(randomKey) - let sshRecord = readSSHRecord() + record.randomKey = await AESEncryptSync(randomKey) + let sshRecord = await readSSHRecord() let idx = sshRecord.findIndex(item => item.host === host) if(idx === -1) sshRecord.push(record) else sshRecord.splice(idx, 1, record) - writeSSHRecord(sshRecord) + await writeSSHRecord(sshRecord) consola.info('新增凭证:', host) res.success({ data: '保存成功' }) } const removeSSH = async ({ res, request }) => { let { body: { host } } = request - let sshRecord = readSSHRecord() + let sshRecord = await readSSHRecord() let idx = sshRecord.findIndex(item => item.host === host) if(idx === -1) return res.fail({ msg: '凭证不存在' }) sshRecord.splice(idx, 1) consola.info('移除凭证:', host) - writeSSHRecord(sshRecord) + await writeSSHRecord(sshRecord) res.success({ data: '移除成功' }) } const existSSH = async ({ res, request }) => { let { body: { host } } = request - let sshRecord = readSSHRecord() - let idx = sshRecord.findIndex(item => item.host === host) + let sshRecord = await readSSHRecord() + let isExist = sshRecord?.some(item => item.host === host) consola.info('查询凭证:', host) - if(idx === -1) return res.success({ data: false }) // host不存在 + if(!isExist) return res.success({ data: false }) // host不存在 res.success({ data: true }) // 存在 } const getCommand = async ({ res, request }) => { let { host } = request.query if(!host) return res.fail({ data: false, msg: '参数错误' }) - let sshRecord = readSSHRecord() - let record = sshRecord.find(item => item.host === host) + let sshRecord = await readSSHRecord() + let record = sshRecord?.find(item => item.host === host) consola.info('查询登录后执行的指令:', host) if(!record) return res.fail({ data: false, msg: 'host not found' }) // host不存在 const { command } = record diff --git a/server/app/controller/user.js b/server/app/controller/user.js index 90d24c4..7ba12a5 100644 --- a/server/app/controller/user.js +++ b/server/app/controller/user.js @@ -1,8 +1,8 @@ const jwt = require('jsonwebtoken') -const { getNetIPInfo, readKey, writeKey, RSADecrypt, AESEncrypt, SHA1Encrypt, sendEmailToConfList, getNotifySwByType } = require('../utils') +const { getNetIPInfo, readKey, writeKey, RSADecryptSync, AESEncryptSync, SHA1Encrypt, sendEmailToConfList, getNotifySwByType } = require('../utils') -const getpublicKey = ({ res }) => { - let { publicKey: data } = readKey() +const getpublicKey = async ({ res }) => { + let { publicKey: data } = await readKey() if(!data) return res.fail({ msg: 'publicKey not found, Try to restart the server', status: 500 }) res.success({ data }) } @@ -50,9 +50,9 @@ const login = async ({ res, request }) => { // 登录流程 try { // console.log('ciphertext', ciphertext) - let password = RSADecrypt(ciphertext) + let password = await RSADecryptSync(ciphertext) // console.log('Decrypt解密password:', password) - let { pwd } = readKey() + let { pwd } = await readKey() if(password === 'admin' && pwd === 'admin') { const token = await beforeLoginHandler(clientIp, jwtExpires) return res.success({ data: { token, jwtExpires }, msg: '登录成功,请及时修改默认密码' }) @@ -72,13 +72,14 @@ const beforeLoginHandler = async (clientIp, jwtExpires) => { // consola.success('登录成功, 准备生成token', new Date()) // 生产token - let { commonKey } = readKey() + let { commonKey } = await readKey() let token = jwt.sign({ date: Date.now() }, commonKey, { expiresIn: jwtExpires }) // 生成token - token = AESEncrypt(token) // 对称加密token后再传输给前端 + token = await AESEncryptSync(token) // 对称加密token后再传输给前端 // 记录客户端登录IP(用于判断是否异地且只保留最近10条) const clientIPInfo = await getNetIPInfo(clientIp) const { ip, country, city } = clientIPInfo || {} + consola.info('登录成功:', new Date(), { ip, country, city }) // 邮件登录通知 let sw = getNotifySwByType('login') @@ -91,14 +92,14 @@ const beforeLoginHandler = async (clientIp, jwtExpires) => { const updatePwd = async ({ res, request }) => { let { body: { oldPwd, newPwd } } = request - let rsaOldPwd = RSADecrypt(oldPwd) + let rsaOldPwd = await RSADecryptSync(oldPwd) oldPwd = rsaOldPwd === 'admin' ? 'admin' : SHA1Encrypt(rsaOldPwd) - let keyObj = readKey() + let keyObj = await readKey() if(oldPwd !== keyObj.pwd) return res.fail({ data: false, msg: '旧密码校验失败' }) // 旧密钥校验通过,加密保存新密码 - newPwd = RSADecrypt(newPwd) === 'admin' ? 'admin' : SHA1Encrypt(RSADecrypt(newPwd)) + newPwd = await RSADecryptSync(newPwd) === 'admin' ? 'admin' : SHA1Encrypt(await RSADecryptSync(newPwd)) keyObj.pwd = newPwd - writeKey(keyObj) + await writeKey(keyObj) let sw = getNotifySwByType('updatePwd') if(sw) sendEmailToConfList('密码修改提醒', '面板登录密码已更改') diff --git a/server/app/db.js b/server/app/db.js new file mode 100644 index 0000000..ba74ae6 --- /dev/null +++ b/server/app/db.js @@ -0,0 +1,154 @@ +const Datastore = require('@seald-io/nedb') +const { resolvePath } = require('./utils/tools') +const { writeKey, writeNotifyList, writeGroupList } = require('./utils/storage') +const { KeyDB, NotifyDB, GroupDB, EmailNotifyDB } = require('./utils/db-class') + +function initKeyDB() { + return new Promise((resolve, reject) => { + const keyDB = new KeyDB().getInstance() + keyDB.count({}, async (err, count) => { + if (err) { + consola.log('初始化keyDB错误:', err) + reject(err) + } else { + if (count === 0) { + consola.log('初始化keyDB✔') + const defaultData = { + pwd: "admin", + commonKey: "", + publicKey: "", + privateKey: "" + } + await writeKey(defaultData) + } + } + resolve() + }) + }) +} + +function initNotifyDB() { + return new Promise((resolve, reject) => { + const notifyDB = new NotifyDB().getInstance() + notifyDB.count({}, async (err, count) => { + if (err) { + consola.log('初始化notifyDB错误:', err) + reject(err) + } else { + if (count === 0) { + consola.log('初始化notifyDB✔') + const defaultData = [{ + "type": "login", + "desc": "登录面板提醒", + "sw": true + }, + { + "type": "err_login", + "desc": "登录错误提醒(连续5次)", + "sw": true + }, + { + "type": "updatePwd", + "desc": "修改密码提醒", + "sw": true + }, + { + "type": "host_offline", + "desc": "客户端离线提醒(每小时最多发送一次提醒)", + "sw": true + }] + await writeNotifyList(defaultData) + } + + } + resolve() + }) + }) +} + +function initGroupDB() { + return new Promise((resolve, reject) => { + const groupDB = new GroupDB().getInstance() + groupDB.count({}, async (err, count) => { + if (err) { + consola.log('初始化groupDB错误:', err) + reject(err) + } else { + if (count === 0) { + consola.log('初始化groupDB✔') + const defaultData = [{ "id": "default", "name": "默认分组", "index": 0 }] + await writeGroupList(defaultData) + } + } + resolve() + }) + }) +} + +function initEmailNotifyDB() { + return new Promise((resolve, reject) => { + const emailNotifyDB = new EmailNotifyDB().getInstance() + emailNotifyDB.count({}, async (err, count) => { + if (err) { + consola.log('初始化emailNotifyDB错误:', err) + reject(err) + } else { + if (count === 0) { + consola.log('初始化emailNotifyDB✔') + const defaultData = { + "support": [ + { + "name": "QQ邮箱", + "target": "qq", + "host": "smtp.qq.com", + "port": 465, + "secure": true, + "tls": { + "rejectUnauthorized": false + } + }, + { + "name": "网易126", + "target": "wangyi126", + "host": "smtp.126.com", + "port": 465, + "secure": true, + "tls": { + "rejectUnauthorized": false + } + }, + { + "name": "网易163", + "target": "wangyi163", + "host": "smtp.163.com", + "port": 465, + "secure": true, + "tls": { + "rejectUnauthorized": false + } + } + ], + "user": [ + ] + } + emailNotifyDB.update({}, { $set: defaultData }, { upsert: true }, (err, numReplaced) => { + if (err) { + reject(err) + } else { + emailNotifyDB.compactDatafile() + resolve(numReplaced) + } + }) + } else { + resolve() + } + } + }) + }) +} +module.exports = async () => { + await initKeyDB() + await initNotifyDB() + await initGroupDB() + await initEmailNotifyDB() +} \ No newline at end of file diff --git a/server/app/storage/README.md b/server/app/db/README.md similarity index 74% rename from server/app/storage/README.md rename to server/app/db/README.md index b200c29..1272dc6 100644 --- a/server/app/storage/README.md +++ b/server/app/db/README.md @@ -1,38 +1,40 @@ -# host-list.json - -> 存储服务器基本信息 - -# key.json - -> 用于加密的密钥相关 - -# ssh-record.json - -> ssh密钥记录(加密存储) - -# email.json - -> 邮件配置 - -- port: 587 --> secure: false -```json -// Gmail调试不通过, 暂缓 -{ - "name": "Google邮箱", - "target": "google", - "host": "smtp.gmail.com", - "port": 465, - "secure": true, - "tls": { - "rejectUnauthorized": false - } -} -``` - -# notify.json - -> 通知配置 - -# group.json - -> 服务器分组配置 +db目录,初始化后自动生成 + +**host-list.db** + +> 存储服务器基本信息 + +**key.db** + +> 用于加密的密钥相关 + +**ssh-record.db** + +> ssh密钥记录(加密存储) + +**email.db** + +> 邮件配置 + +- port: 587 --> secure: false +```db +// Gmail调试不通过, 暂缓 +{ + "name": "Google邮箱", + "target": "google", + "host": "smtp.gmail.com", + "port": 465, + "secure": true, + "tls": { + "rejectUnauthorized": false + } +} +``` + +**notify.db** + +> 通知配置 + +**group.db** + +> 服务器分组配置 diff --git a/server/app/init.js b/server/app/init.js index 5d246ac..d560706 100644 --- a/server/app/init.js +++ b/server/app/init.js @@ -1,5 +1,5 @@ const NodeRSA = require('node-rsa') -const { getNetIPInfo, readHostList, writeHostList, readKey, writeKey, randomStr, isProd, AESEncrypt } = require('./utils') +const { getNetIPInfo, readHostList, writeHostList, readKey, writeKey, randomStr, isProd, AESEncryptSync } = require('./utils') const isDev = !isProd() @@ -7,7 +7,7 @@ const isDev = !isProd() async function initLocalIp() { if(isDev) return consola.info('非生产环境不初始化保存本地IP') const localNetIPInfo = await getNetIPInfo() - let vpsList = readHostList() + let vpsList = await readHostList() let { ip: localNetIP } = localNetIPInfo if(vpsList.some(({ host }) => host === localNetIP)) return consola.info('本机IP已储存: ', localNetIP) vpsList.unshift({ name: 'server-side-host', host: localNetIP, group: 'default' }) @@ -17,33 +17,32 @@ async function initLocalIp() { // 初始化公私钥, 供登录、保存ssh密钥/密码等加解密 async function initRsa() { - let keyObj = readKey() + let keyObj = await readKey() if(keyObj.privateKey && keyObj.publicKey) return consola.info('公私钥已存在[重新生成会导致已保存的ssh密钥信息失效]') - let key = new NodeRSA({ b: 1024 }) key.setOptions({ encryptionScheme: 'pkcs1' }) let privateKey = key.exportKey('pkcs1-private-pem') let publicKey = key.exportKey('pkcs8-public-pem') - keyObj.privateKey = AESEncrypt(privateKey) // 加密私钥 + keyObj.privateKey = await AESEncryptSync(privateKey) // 加密私钥 keyObj.publicKey = publicKey // 公开公钥 - writeKey(keyObj) + await writeKey(keyObj) consola.info('Task: 已生成新的非对称加密公私钥') } // 随机的commonKey secret -function randomJWTSecret() { - let keyObj = readKey() - if(keyObj.commonKey) return consola.info('commonKey密钥已存在') +async function randomJWTSecret() { + let keyObj = await readKey() + if(keyObj?.commonKey) return consola.info('commonKey密钥已存在') keyObj.commonKey = randomStr(16) - writeKey(keyObj) + await writeKey(keyObj) consola.info('Task: 已生成新的随机commonKey密钥') } -module.exports = () => { - randomJWTSecret() // 先生成全局唯一密钥 - initLocalIp() - initRsa() +module.exports = async () => { + await randomJWTSecret() // 全局密钥 + await initRsa() // 全局公钥密钥 + // initLocalIp() // :TODO: 默认添加服务端vps // 用于记录客户端登录IP的列表 global.loginRecord = [] } diff --git a/server/app/main.js b/server/app/main.js index 58e141e..9426ede 100644 --- a/server/app/main.js +++ b/server/app/main.js @@ -1,13 +1,16 @@ const consola = require('consola') global.consola = consola const { httpServer, clientHttpServer } = require('./server') -const initLocal = require('./init') -const scheduleJob = require('./schedule') +const initDB = require('./db') +const initEncryptConf = require('./init') +// const scheduleJob = require('./schedule') -scheduleJob() +async function main() { + await initDB() + await initEncryptConf() + httpServer() + clientHttpServer() + // scheduleJob() +} -initLocal() - -httpServer() - -clientHttpServer() +main() diff --git a/server/app/middlewares/auth.js b/server/app/middlewares/auth.js index d91786a..9d7a66a 100644 --- a/server/app/middlewares/auth.js +++ b/server/app/middlewares/auth.js @@ -1,4 +1,4 @@ -const { verifyAuth } = require('../utils') +const { verifyAuthSync } = require('../utils') const { apiPrefix } = require('../config') let whitePath = [ @@ -13,7 +13,7 @@ const useAuth = async ({ request, res }, next) => { if(whitePath.includes(path)) return next() if(!token) return res.fail({ msg: '未登录', status: 403 }) // 验证token - const { code, msg } = verifyAuth(token, request.ip) + const { code, msg } = await verifyAuthSync(token, request.ip) switch(code) { case 1: return await next() diff --git a/server/app/middlewares/compress.js b/server/app/middlewares/compress.js index 1bcb27c..0595dad 100644 --- a/server/app/middlewares/compress.js +++ b/server/app/middlewares/compress.js @@ -1,3 +1,4 @@ +// 响应压缩模块,自适应头部压缩方式 const compress = require('koa-compress') const options = { threshold: 2048 } diff --git a/server/app/router/routes.js b/server/app/router/routes.js index 76eab6e..52d2d49 100644 --- a/server/app/router/routes.js +++ b/server/app/router/routes.js @@ -88,13 +88,13 @@ const notify = [ }, { method: 'post', - path: '/push-email', - controller: pushEmail + path: '/user-email', + controller: updateUserEmailList }, { method: 'post', - path: '/user-email', - controller: updateUserEmailList + path: '/push-email', + controller: pushEmail }, { method: 'delete', diff --git a/server/app/schedule/expired-notify.js b/server/app/schedule/expired-notify.js index 40faa5f..c33cedf 100644 --- a/server/app/schedule/expired-notify.js +++ b/server/app/schedule/expired-notify.js @@ -3,7 +3,7 @@ const { readHostList, sendEmailToConfList, formatTimestamp } = require('../utils const expiredNotifyJob = () => { consola.info('=====开始检测服务器到期时间=====', new Date()) - const hostList = readHostList() + const hostList = await readHostList() for (const item of hostList) { if(!item.expiredNotify) continue const { host, name, expired, consoleUrl } = item diff --git a/server/app/schedule/offline-inspect.js b/server/app/schedule/offline-inspect.js index 972a821..4232c32 100644 --- a/server/app/schedule/offline-inspect.js +++ b/server/app/schedule/offline-inspect.js @@ -4,11 +4,12 @@ const { readHostList, sendEmailToConfList, getNotifySwByType, formatTimestamp, i const testConnectAsync = require('../utils/test-connect') let sendNotifyRecord = new Map() -const offlineJob = () => { +const offlineJob = async () => { let sw = getNotifySwByType('host_offline') if(!sw) return consola.info('=====开始检测服务器状态=====', new Date()) - for (const item of readHostList()) { + const hostList = await readHostList() + for (const item of hostList) { const { host, name } = item // consola.info('start inpect:', host, name ) testConnectAsync({ diff --git a/server/app/socket/clients.js b/server/app/socket/clients.js index 9bc3434..62e35e4 100644 --- a/server/app/socket/clients.js +++ b/server/app/socket/clients.js @@ -2,15 +2,15 @@ const { Server: ServerIO } = require('socket.io') const { io: ClientIO } = require('socket.io-client') const { readHostList } = require('../utils') const { clientPort } = require('../config') -const { verifyAuth } = require('../utils') +const { verifyAuthSync } = require('../utils') let clientSockets = {}, clientsData = {} -function getClientsInfo(socketId) { - let hostList = readHostList() +async function getClientsInfo(socketId) { + let hostList = await readHostList() hostList - .map(({ host, name }) => { - let clientSocket = ClientIO(`http://${ host }:${ clientPort }`, { + ?.map(({ host, name }) => { + let clientSocket = ClientIO(`http://${host}:${clientPort}`, { path: '/client/os-info', forceNew: true, timeout: 5000, @@ -58,10 +58,10 @@ module.exports = (httpServer) => { serverIo.on('connection', (socket) => { // 前者兼容nginx反代, 后者兼容nodejs自身服务 let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address - socket.on('init_clients_data', ({ token }) => { + socket.on('init_clients_data', async ({ token }) => { // 校验登录态 - const { code, msg } = verifyAuth(token, clientIp) - if(code !== 1) { + const { code, msg } = await verifyAuthSync(token, clientIp) + if (code !== 1) { socket.emit('token_verify_fail', msg || '鉴权失败') socket.disconnect() return @@ -86,7 +86,7 @@ module.exports = (httpServer) => { // 关闭连接 socket.on('disconnect', () => { // 防止内存泄漏 - if(timer) clearInterval(timer) + if (timer) clearInterval(timer) // 当web端与服务端断开连接时, 服务端与每个客户端的socket也应该断开连接 clientSockets[socket.id].forEach(socket => socket.close && socket.close()) delete clientSockets[socket.id] diff --git a/server/app/socket/host-status.js b/server/app/socket/host-status.js index 9524721..95362f8 100644 --- a/server/app/socket/host-status.js +++ b/server/app/socket/host-status.js @@ -1,7 +1,7 @@ const { Server: ServerIO } = require('socket.io') const { io: ClientIO } = require('socket.io-client') const { clientPort } = require('../config') -const { verifyAuth } = require('../utils') +const { verifyAuthSync } = require('../utils') let hostSockets = {} @@ -47,9 +47,9 @@ module.exports = (httpServer) => { serverIo.on('connection', (serverSocket) => { // 前者兼容nginx反代, 后者兼容nodejs自身服务 let clientIp = serverSocket.handshake.headers['x-forwarded-for'] || serverSocket.handshake.address - serverSocket.on('init_host_data', ({ token, host }) => { + serverSocket.on('init_host_data', async ({ token, host }) => { // 校验登录态 - const { code, msg } = verifyAuth(token, clientIp) + const { code, msg } = await verifyAuthSync(token, clientIp) if(code !== 1) { serverSocket.emit('token_verify_fail', msg || '鉴权失败') serverSocket.disconnect() diff --git a/server/app/socket/monitor.js b/server/app/socket/monitor.js index edcbded..5a45a89 100644 --- a/server/app/socket/monitor.js +++ b/server/app/socket/monitor.js @@ -60,7 +60,7 @@ module.exports = (httpServer) => { }, 1000) socket.on('disconnect', () => { - // 断开时清楚对应的websocket连接 + // 断开时清除对应的websocket连接 if(serverSockets[socket.id]) clearInterval(serverSockets[socket.id]) delete serverSockets[socket.id] socket.close && socket.close() diff --git a/server/app/socket/sftp.js b/server/app/socket/sftp.js index bbb489d..c78b605 100644 --- a/server/app/socket/sftp.js +++ b/server/app/socket/sftp.js @@ -2,7 +2,7 @@ const { Server } = require('socket.io') const SFTPClient = require('ssh2-sftp-client') const rawPath = require('path') const fs = require('fs') -const { readSSHRecord, verifyAuth, RSADecrypt, AESDecrypt } = require('../utils') +const { readSSHRecord, verifyAuthSync, RSADecryptSync, AESDecryptSync } = require('../utils') const { sftpCacheDir } = require('../config') const CryptoJS = require('crypto-js') @@ -205,21 +205,21 @@ module.exports = (httpServer) => { let sftpClient = new SFTPClient() consola.success('terminal websocket 已连接') - socket.on('create', ({ host: ip, token }) => { - const { code } = verifyAuth(token, clientIp) + socket.on('create', async ({ host: ip, token }) => { + const { code } = await verifyAuthSync(token, clientIp) if(code !== 1) { socket.emit('token_verify_fail') socket.disconnect() return } - const sshRecord = readSSHRecord() + const sshRecord = await readSSHRecord() let loginInfo = sshRecord.find(item => item.host === ip) if(!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ ip }】凭证`) let { type, host, port, username, randomKey } = loginInfo // 解密放到try里面,防止报错【公私钥必须配对, 否则需要重新添加服务器密钥】 - randomKey = AESDecrypt(randomKey) // 先对称解密key - randomKey = RSADecrypt(randomKey) // 再非对称解密key - loginInfo[type] = AESDecrypt(loginInfo[type], randomKey) // 对称解密ssh密钥 + randomKey = await AESDecryptSync(randomKey) // 先对称解密key + randomKey = await RSADecryptSync(randomKey) // 再非对称解密key + loginInfo[type] = await AESDecryptSync(loginInfo[type], randomKey) // 对称解密ssh密钥 consola.info('准备连接Sftp:', host) const authInfo = { host, port, username, [type]: loginInfo[type] } sftpClient.connect(authInfo) diff --git a/server/app/socket/terminal.js b/server/app/socket/terminal.js index 833cde5..41b9750 100644 --- a/server/app/socket/terminal.js +++ b/server/app/socket/terminal.js @@ -1,6 +1,6 @@ const { Server } = require('socket.io') const { Client: SSHClient } = require('ssh2') -const { readSSHRecord, verifyAuth, RSADecrypt, AESDecrypt } = require('../utils') +const { readSSHRecord, verifyAuthSync, RSADecryptSync, AESDecryptSync } = require('../utils') function createTerminal(socket, sshClient) { sshClient.shell({ term: 'xterm-color' }, (err, stream) => { @@ -16,7 +16,7 @@ function createTerminal(socket, sshClient) { }) // web端输入 socket.on('input', key => { - if(sshClient._sock.writable === false) return consola.info('终端连接已关闭') + if (sshClient._sock.writable === false) return consola.info('终端连接已关闭') stream.write(key) }) socket.emit('connect_terminal') // 已连接终端,web端可以执行指令了 @@ -42,35 +42,40 @@ module.exports = (httpServer) => { let sshClient = new SSHClient() consola.success('terminal websocket 已连接') - socket.on('create', ({ host: ip, token }) => { - const { code } = verifyAuth(token, clientIp) - if(code !== 1) { + socket.on('create', async ({ host: ip, token }) => { + const { code } = await verifyAuthSync(token, clientIp) + if (code !== 1) { socket.emit('token_verify_fail') socket.disconnect() return } - const sshRecord = readSSHRecord() + const sshRecord = await readSSHRecord() let loginInfo = sshRecord.find(item => item.host === ip) - if(!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ ip }】凭证`) + if (!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ip}】凭证`) let { type, host, port, username, randomKey } = loginInfo try { // 解密放到try里面,防止报错【公私钥必须配对, 否则需要重新添加服务器密钥】 - randomKey = AESDecrypt(randomKey) // 先对称解密key - randomKey = RSADecrypt(randomKey) // 再非对称解密key - loginInfo[type] = AESDecrypt(loginInfo[type], randomKey) // 对称解密ssh密钥 + randomKey = await AESDecryptSync(randomKey) // 先对称解密key + randomKey = await RSADecryptSync(randomKey) // 再非对称解密key + loginInfo[type] = await AESDecryptSync(loginInfo[type], randomKey) // 对称解密ssh密钥 consola.info('准备连接终端:', host) - const authInfo = { host, port, username, [type]: loginInfo[type] } + const authInfo = { host, port, username, [type]: loginInfo[type] } // .replace(/\n/g, '') + // console.log(authInfo) sshClient .on('ready', () => { consola.success('已连接到终端:', host) - socket.emit('connect_success', `已连接到终端:${ host }`) + socket.emit('connect_success', `已连接到终端:${host}`) createTerminal(socket, sshClient) }) .on('error', (err) => { + console.log(err) consola.error('连接终端失败:', err.level) socket.emit('connect_fail', err.message) }) - .connect(authInfo) + .connect({ + ...authInfo, + // debug: (info) => console.log(info) + }) } catch (err) { consola.error('创建终端失败:', err.message) socket.emit('create_fail', err.message) diff --git a/server/app/storage/email.json b/server/app/storage/email.json deleted file mode 100644 index b39d9d1..0000000 --- a/server/app/storage/email.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "support": [ - { - "name": "QQ邮箱", - "target": "qq", - "host": "smtp.qq.com", - "port": 465, - "secure": true, - "tls": { - "rejectUnauthorized": false - } - }, - { - "name": "网易126", - "target": "wangyi126", - "host": "smtp.126.com", - "port": 465, - "secure": true, - "tls": { - "rejectUnauthorized": false - } - }, - { - "name": "网易163", - "target": "wangyi163", - "host": "smtp.163.com", - "port": 465, - "secure": true, - "tls": { - "rejectUnauthorized": false - } - } - ], - "user": [ - ] -} \ No newline at end of file diff --git a/server/app/storage/group.json b/server/app/storage/group.json deleted file mode 100644 index 3691318..0000000 --- a/server/app/storage/group.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "id": "default", - "name": "默认分组", - "index": 0 - } -] \ No newline at end of file diff --git a/server/app/storage/host-list.json b/server/app/storage/host-list.json deleted file mode 100644 index 0637a08..0000000 --- a/server/app/storage/host-list.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/server/app/storage/key.json b/server/app/storage/key.json deleted file mode 100644 index f66c629..0000000 --- a/server/app/storage/key.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "pwd": "admin", - "commonKey": "", - "publicKey": "", - "privateKey": "" -} \ No newline at end of file diff --git a/server/app/storage/notify.json b/server/app/storage/notify.json deleted file mode 100644 index 74a99af..0000000 --- a/server/app/storage/notify.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "type": "login", - "desc": "登录面板提醒", - "sw": true - }, - { - "type": "err_login", - "desc": "登录错误提醒(连续5次)", - "sw": true - }, - { - "type": "updatePwd", - "desc": "修改密码提醒", - "sw": true - }, - { - "type": "host_offline", - "desc": "客户端离线提醒(每小时最多发送一次提醒)", - "sw": true - } -] \ No newline at end of file diff --git a/server/app/storage/ssh-record.json b/server/app/storage/ssh-record.json deleted file mode 100644 index 0637a08..0000000 --- a/server/app/storage/ssh-record.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/server/app/utils/db-class.js b/server/app/utils/db-class.js new file mode 100644 index 0000000..effc3be --- /dev/null +++ b/server/app/utils/db-class.js @@ -0,0 +1,68 @@ +const Datastore = require('@seald-io/nedb') +const { sshRecordDBPath, hostListDBPath, keyDBPath, emailNotifyDBPath, notifyConfDBPath, groupConfDBPath } = require('../config') + +module.exports.KeyDB = class KeyDB { + constructor() { + if (!KeyDB.instance) { + KeyDB.instance = new Datastore({ filename: keyDBPath, autoload: true }) + } + } + getInstance() { + return KeyDB.instance + } +} + +module.exports.HostListDB = class HostListDB { + constructor() { + if (!HostListDB.instance) { + HostListDB.instance = new Datastore({ filename: hostListDBPath, autoload: true }) + } + } + getInstance() { + return HostListDB.instance + } +} + +module.exports.SshRecordDB = class SshRecordDB { + constructor() { + if (!SshRecordDB.instance) { + SshRecordDB.instance = new Datastore({ filename: sshRecordDBPath, autoload: true }) + } + } + getInstance() { + return SshRecordDB.instance + } +} + +module.exports.NotifyDB = class NotifyDB { + constructor() { + if (!NotifyDB.instance) { + NotifyDB.instance = new Datastore({ filename: notifyConfDBPath, autoload: true }) + } + } + getInstance() { + return NotifyDB.instance + } +} + +module.exports.GroupDB = class GroupDB { + constructor() { + if (!GroupDB.instance) { + GroupDB.instance = new Datastore({ filename: groupConfDBPath, autoload: true }) + } + } + getInstance() { + return GroupDB.instance + } +} + +module.exports.EmailNotifyDB = class EmailNotifyDB { + constructor() { + if (!EmailNotifyDB.instance) { + EmailNotifyDB.instance = new Datastore({ filename: emailNotifyDBPath, autoload: true }) + } + } + getInstance() { + return EmailNotifyDB.instance + } +} diff --git a/server/app/utils/email.js b/server/app/utils/email.js index 7c27e38..13a9160 100644 --- a/server/app/utils/email.js +++ b/server/app/utils/email.js @@ -11,12 +11,12 @@ const emailTransporter = async (params = {}) => { let { toEmail, title, html } = params try { if(!toEmail) throw Error('missing params: toEmail') - let userEmail = readUserEmailList().find(({ auth }) => auth.user === toEmail) + let userEmail = (await readUserEmailList()).find(({ auth }) => auth.user === toEmail) if(!userEmail) throw Error(`${ toEmail } 不存在已保存的配置文件中, 请移除后重新添加`) let { target } = userEmail - let emailServerConf = readSupportEmailList().find((item) => item.target === target) + let emailServerConf = (await readSupportEmailList()).find((item) => item.target === target) if(!emailServerConf) throw Error(`邮箱类型不支持:${ target }`) - const timeout = 1000*6 + const timeout = 1000*5 let options = Object.assign({}, userEmail, emailServerConf, { greetingTimeout: timeout, connectionTimeout: timeout }) let transporter = nodemailer.createTransport(options) let info = await transporter.sendMail({ @@ -36,7 +36,7 @@ const emailTransporter = async (params = {}) => { const sendEmailToConfList = (title, content) => { // eslint-disable-next-line return new Promise(async (res, rej) => { - let emailList = readUserEmailList() + let emailList = await readUserEmailList() if(Array.isArray(emailList) && emailList.length >= 1) { for (const item of emailList) { const toEmail = item.auth.user diff --git a/server/app/utils/encrypt.js b/server/app/utils/encrypt.js index 4622545..ac601cd 100644 --- a/server/app/utils/encrypt.js +++ b/server/app/utils/encrypt.js @@ -4,10 +4,10 @@ const NodeRSA = require('node-rsa') const { readKey } = require('./storage.js') // rsa非对称 私钥解密 -const RSADecrypt = (ciphertext) => { - if(!ciphertext) return - let { privateKey } = readKey() - privateKey = AESDecrypt(privateKey) // 先解密私钥 +const RSADecryptSync = async (ciphertext) => { + if (!ciphertext) return + let { privateKey } = await readKey() + privateKey = await AESDecryptSync(privateKey) // 先解密私钥 const rsakey = new NodeRSA(privateKey) rsakey.setOptions({ encryptionScheme: 'pkcs1' }) // Must Set It When Frontend Use jsencrypt const plaintext = rsakey.decrypt(ciphertext, 'utf8') @@ -15,17 +15,17 @@ const RSADecrypt = (ciphertext) => { } // aes对称 加密(default commonKey) -const AESEncrypt = (text, key) => { +const AESEncryptSync = async (text, key) => { if(!text) return - let { commonKey } = readKey() + let { commonKey } = await readKey() let ciphertext = CryptoJS.AES.encrypt(text, key || commonKey).toString() return ciphertext } // aes对称 解密(default commonKey) -const AESDecrypt = (ciphertext, key) => { +const AESDecryptSync = async (ciphertext, key) => { if(!ciphertext) return - let { commonKey } = readKey() + let { commonKey } = await readKey() let bytes = CryptoJS.AES.decrypt(ciphertext, key || commonKey) let originalText = bytes.toString(CryptoJS.enc.Utf8) return originalText @@ -37,8 +37,8 @@ const SHA1Encrypt = (clearText) => { } module.exports = { - RSADecrypt, - AESEncrypt, - AESDecrypt, + RSADecryptSync, + AESEncryptSync, + AESDecryptSync, SHA1Encrypt } \ No newline at end of file diff --git a/server/app/utils/index.js b/server/app/utils/index.js index 4be575e..68d7996 100644 --- a/server/app/utils/index.js +++ b/server/app/utils/index.js @@ -12,9 +12,10 @@ const { getNotifySwByType, writeNotifyList, readGroupList, - writeGroupList } = require('./storage') -const { RSADecrypt, AESEncrypt, AESDecrypt, SHA1Encrypt } = require('./encrypt') -const { verifyAuth, isProd } = require('./verify-auth') + writeGroupList +} = require('./storage') +const { RSADecryptSync, AESEncryptSync, AESDecryptSync, SHA1Encrypt } = require('./encrypt') +const { verifyAuthSync, isProd } = require('./verify-auth') const { getNetIPInfo, throwError, isIP, randomStr, getUTCDate, formatTimestamp } = require('./tools') const { emailTransporter, sendEmailToConfList } = require('./email') @@ -25,11 +26,11 @@ module.exports = { randomStr, getUTCDate, formatTimestamp, - verifyAuth, + verifyAuthSync, isProd, - RSADecrypt, - AESEncrypt, - AESDecrypt, + RSADecryptSync, + AESEncryptSync, + AESDecryptSync, SHA1Encrypt, readSSHRecord, writeSSHRecord, diff --git a/server/app/utils/storage.js b/server/app/utils/storage.js index 0d4635c..58218d2 100644 --- a/server/app/utils/storage.js +++ b/server/app/utils/storage.js @@ -1,124 +1,239 @@ const fs = require('fs') -const { sshRecordPath, hostListPath, keyPath, emailPath, notifyPath, groupPath } = require('../config') +const { sshRecordDBPath, hostListDBPath, keyDBPath, emailNotifyDBPath, notifyConfDBPath, groupConfDBPath } = require('../config') +const { KeyDB, HostListDB, SshRecordDB, NotifyDB, GroupDB, EmailNotifyDB } = require('./db-class') -const readSSHRecord = () => { - let list +const readKey = async () => { + return new Promise((resolve, reject) => { + const keyDB = new KeyDB().getInstance() + keyDB.findOne({}, (err, doc) => { + if (err) { + reject(err) + } else { + resolve(doc) + } + }) + }) +} + +const writeKey = async (keyObj = {}) => { + const keyDB = new KeyDB().getInstance() + return new Promise((resolve, reject) => { + keyDB.update({}, { $set: keyObj }, { upsert: true }, (err, numReplaced) => { + if (err) { + reject(err) + } else { + keyDB.compactDatafile() + resolve(numReplaced) + } + }) + }) +} + +const readSSHRecord = async () => { + const sshRecordDB = new SshRecordDB().getInstance() + return new Promise((resolve, reject) => { + sshRecordDB.find({}, (err, docs) => { + if (err) { + consola.error('读取ssh-record-db错误: ', err) + reject(err) + } else { + resolve(docs) + } + }) + }) +} + +const writeSSHRecord = async (record = []) => { + return new Promise((resolve, reject) => { + const sshRecordDB = new SshRecordDB().getInstance() + sshRecordDB.remove({}, { multi: true }, (err, numRemoved) => { + if (err) { + consola.error('清空SSHRecord出错:', err) + reject(err) + } else { + sshRecordDB.insert(record, (err, newDocs) => { + if (err) { + consola.error('写入新的ssh记录出错:', err) + reject(err) + } else { + sshRecordDB.compactDatafile() + resolve(newDocs) + } + }) + } + }) + }) +} + +const readHostList = async () => { + return new Promise((resolve, reject) => { + const hostListDB = new HostListDB().getInstance() + hostListDB.find({}, (err, docs) => { + if (err) { + consola.error('读取host-list-db错误:', err) + reject(err) + } else { + resolve(docs) + } + }) + }) +} + +const writeHostList = async (record = []) => { + return new Promise((resolve, reject) => { + const hostListDB = new HostListDB().getInstance() + hostListDB.remove({}, { multi: true }, (err, numRemoved) => { + if (err) { + consola.error('清空HostList出错:', err) + reject(err) + } else { + // 插入新的数据列表 + hostListDB.insert(record, (err, newDocs) => { + if (err) { + consola.error('写入新的HostList出错:', err) + reject(err); + } else { + hostListDB.compactDatafile() + resolve(newDocs) + } + }) + } + }) + }) +} + +const readEmailNotifyConf = () => { + return new Promise((resolve, reject) => { + const emailNotifyDB = new EmailNotifyDB().getInstance() + emailNotifyDB.findOne({}, (err, docs) => { + if (err) { + consola.error('读取email-notify-conf-db错误:', err) + reject(err) + } else { + resolve(docs) + } + }) + }) +} +const writeUserEmailList = (user) => { + const emailNotifyDB = new EmailNotifyDB().getInstance() + return new Promise(async (resolve, reject) => { + let support = await readSupportEmailList() + const emailConf = { support, user } + emailNotifyDB.update({}, { $set: emailConf }, { upsert: true }, (err, numReplaced) => { + if (err) { + reject({ code: -1, msg: err.message || err }) + } else { + emailNotifyDB.compactDatafile() + resolve({ code: 0 }) + } + }) + }) +} + +const readSupportEmailList = async () => { + let support = [] try { - list = JSON.parse(fs.readFileSync(sshRecordPath, 'utf8')) - } catch (error) { - consola.error('读取ssh-record错误, 即将重置ssh列表: ', error) - writeSSHRecord([]) - } - return list || [] -} - -const writeSSHRecord = (record = []) => { - fs.writeFileSync(sshRecordPath, JSON.stringify(record, null, 2)) -} - -const readHostList = () => { - let list - try { - list = JSON.parse(fs.readFileSync(hostListPath, 'utf8')) - } catch (error) { - consola.error('读取host-list错误, 即将重置host列表: ', error) - writeHostList([]) - } - return list || [] -} - -const writeHostList = (record = []) => { - fs.writeFileSync(hostListPath, JSON.stringify(record, null, 2)) -} - -const readKey = () => { - let keyObj = JSON.parse(fs.readFileSync(keyPath, 'utf8')) - return keyObj -} - -const writeKey = (keyObj = {}) => { - fs.writeFileSync(keyPath, JSON.stringify(keyObj, null, 2)) -} - -const readEmailJson = () => { - let emailJson = {} - try { - emailJson = JSON.parse(fs.readFileSync(emailPath, 'utf8')) - } catch (error) { - consola.error('读取email.json错误: ', error) - } - return emailJson -} - -const readSupportEmailList = () => { - let supportEmailList = [] - try { - supportEmailList = readEmailJson().support + support = (await readEmailNotifyConf()).support } catch (error) { consola.error('读取email support错误: ', error) } - return supportEmailList + return support } -const readUserEmailList = () => { - let configEmailList = [] +const readUserEmailList = async () => { + let user = [] try { - configEmailList = readEmailJson().user + user = (await readEmailNotifyConf()).user } catch (error) { consola.error('读取email config错误: ', error) } - return configEmailList + return user } -const writeUserEmailList = (user) => { - let support = readSupportEmailList() - const emailJson = { support, user } - try { - fs.writeFileSync(emailPath, JSON.stringify(emailJson, null, 2)) - return { code: 0 } - } catch (error) { - return { code: -1, msg: error.message || error } - } -} -const readNotifyList = () => { - let notifyList = [] +const getNotifySwByType = async (type) => { + if (!type) throw Error('missing params: type') try { - notifyList = JSON.parse(fs.readFileSync(notifyPath, 'utf8')) - } catch (error) { - consola.error('读取notify list错误: ', error) - } - return notifyList -} - -const getNotifySwByType = (type) => { - if(!type) throw Error('missing params: type') - try { - let { sw } = readNotifyList().find((item) => item.type === type) + let notifyList = await readNotifyList() + let { sw } = notifyList.find((item) => item.type === type) return sw } catch (error) { - consola.error(`通知类型[${ type }]不存在`) + consola.error(`通知类型[${type}]不存在`) return false } } -const writeNotifyList = (notifyList) => { - fs.writeFileSync(notifyPath, JSON.stringify(notifyList, null, 2)) +const readNotifyList = async () => { + return new Promise((resolve, reject) => { + const notifyDB = new NotifyDB().getInstance() + notifyDB.find({}, (err, docs) => { + if (err) { + consola.error('读取notify list错误: ', err) + reject(err) + } else { + resolve(docs) + } + }) + }) } -const readGroupList = () => { - let list - try { - list = JSON.parse(fs.readFileSync(groupPath, 'utf8')) - } catch (error) { - consola.error('读取group-list错误, 即将重置group列表: ', error) - writeSSHRecord([]) - } - return list || [] +const writeNotifyList = async (notifyList) => { + return new Promise((resolve, reject) => { + const notifyDB = new NotifyDB().getInstance() + notifyDB.remove({}, { multi: true }, (err, numRemoved) => { + if (err) { + consola.error('清空notify list出错:', err); + reject(err); + } else { + notifyDB.insert(notifyList, (err, newDocs) => { + if (err) { + consola.error('写入新的notify list出错:', err); + reject(err) + } else { + notifyDB.compactDatafile() + resolve(newDocs); + } + }) + } + }) + }) } -const writeGroupList = (list = []) => { - fs.writeFileSync(groupPath, JSON.stringify(list, null, 2)) +const readGroupList = async () => { + return new Promise((resolve, reject) => { + const groupDB = new GroupDB().getInstance() + groupDB.find({}, (err, docs) => { + if (err) { + consola.error('读取group list错误: ', err) + reject(err) + } else { + resolve(docs) + } + }) + }) +} + +const writeGroupList = async (list = []) => { + return new Promise((resolve, reject) => { + const groupDB = new GroupDB().getInstance() + groupDB.remove({}, { multi: true }, (err, numRemoved) => { + if (err) { + consola.error('清空group list出错:', err) + reject(err) + } else { + groupDB.insert(list, (err, newDocs) => { + if (err) { + consola.error('写入新的group list出错:', err) + reject(err) + } else { + groupDB.compactDatafile() + resolve(newDocs) + } + }) + } + }) + }) } module.exports = { @@ -128,12 +243,12 @@ module.exports = { writeHostList, readKey, writeKey, - readSupportEmailList, - readUserEmailList, - writeUserEmailList, readNotifyList, getNotifySwByType, writeNotifyList, readGroupList, - writeGroupList + writeGroupList, + readSupportEmailList, + readUserEmailList, + writeUserEmailList } \ No newline at end of file diff --git a/server/app/utils/tools.js b/server/app/utils/tools.js index 3b3651e..9cb34a3 100644 --- a/server/app/utils/tools.js +++ b/server/app/utils/tools.js @@ -1,10 +1,19 @@ +const fs = require('fs') +const net = require('net') const axios = require('axios') const request = axios.create({ timeout: 3000 }) // 为空时请求本地IP const getNetIPInfo = async (searchIp = '') => { - searchIp = searchIp.replace(/::ffff:/g, '') || '' // fix: nginx反代 - if(['::ffff:', '::1'].includes(searchIp)) searchIp = '127.0.0.1' + console.log('searchIp:', searchIp) + if (isLocalIP(searchIp)) { + return { + ip: searchIp, + country: '本地', + city: '局域网', + error: null + } + } try { let date = Date.now() let ipUrls = [ @@ -63,7 +72,7 @@ const getNetIPInfo = async (searchIp = '') => { let { origip: ip, location: country, city = '', regionName = '' } = res || {} searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date }) } - // console.log(searchResult) + console.log(searchResult) let validInfo = searchResult.find(item => Boolean(item.country)) consola.info('查询IP信息:', validInfo) return validInfo || { ip: '获取IP信息API出错,请排查或更新API', country: '未知', city: '未知', date } @@ -78,6 +87,64 @@ const getNetIPInfo = async (searchIp = '') => { } } +function isLocalIP(ip) { + // Check if IPv4 or IPv6 address + const isIPv4 = net.isIPv4(ip) + const isIPv6 = net.isIPv6(ip) + + // Local IPv4 ranges + const localIPv4Ranges = [ + { start: '10.0.0.0', end: '10.255.255.255' }, + { start: '172.16.0.0', end: '172.31.255.255' }, + { start: '192.168.0.0', end: '192.168.255.255' }, + { start: '127.0.0.0', end: '127.255.255.255' } // Loopback + ] + + // Local IPv6 ranges + const localIPv6Ranges = [ + '::1', // Loopback + 'fc00::', // Unique local address + 'fd00::' // Unique local address + ]; + + function isInRange(ip, start, end) { + const ipNum = ipToNumber(ip) + return ipNum >= ipToNumber(start) && ipNum <= ipToNumber(end) + } + + function ipToNumber(ip) { + return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) + } + + if (isIPv4) { + for (const range of localIPv4Ranges) { + if (isInRange(ip, range.start, range.end)) { + return true + } + } + } + + if (isIPv6) { + if (localIPv6Ranges.includes(ip)) { + return true + } + + // Handle IPv4-mapped IPv6 addresses (e.g., ::ffff:192.168.1.1) + if (ip.startsWith('::ffff:')) { + const ipv4Part = ip.split('::ffff:')[1] + if (ipv4Part && net.isIPv4(ipv4Part)) { + for (const range of localIPv4Ranges) { + if (isInRange(ipv4Part, range.start, range.end)) { + return true + } + } + } + } + } + + return false +} + const throwError = ({ status = 500, msg = 'defalut error' } = {}) => { const err = new Error(msg) err.status = status // 主动抛错 @@ -134,11 +201,16 @@ const formatTimestamp = (timestamp = Date.now(), format = 'time') => { } } +function resolvePath(dir, path) { + return path.resolve(dir, path) +} + module.exports = { getNetIPInfo, throwError, isIP, randomStr, getUTCDate, - formatTimestamp + formatTimestamp, + resolvePath } \ No newline at end of file diff --git a/server/app/utils/verify-auth.js b/server/app/utils/verify-auth.js index 0a65c3a..56da11e 100644 --- a/server/app/utils/verify-auth.js +++ b/server/app/utils/verify-auth.js @@ -1,5 +1,5 @@ -const { AESDecrypt } = require('./encrypt') +const { AESDecryptSync } = require('./encrypt') const { readKey } = require('./storage') const jwt = require('jsonwebtoken') @@ -10,21 +10,13 @@ const enumLoginCode = { } // 校验token与登录IP -const verifyAuth = (token, clientIp) =>{ - if(['::ffff:', '::1'].includes(clientIp)) clientIp = '127.0.0.1' - token = AESDecrypt(token) // 先aes解密 - const { commonKey } = readKey() +const verifyAuthSync = async (token, clientIp) => { + consola.info('verifyAuthSync IP:', clientIp) + token = await AESDecryptSync(token) // 先aes解密 + const { commonKey } = await readKey() try { const { exp } = jwt.verify(token, commonKey) - if(Date.now() > (exp * 1000)) return { code: -1, msg: 'token expires' } // 过期 - - let lastLoginIp = global.loginRecord[0] ? global.loginRecord[0].ip : '' - consola.info('校验客户端IP:', clientIp) - consola.info('最后登录的IP:', lastLoginIp) - // 判断: (生产环境)clientIp与上次登录成功IP不一致 - if(isProd() && (!lastLoginIp || !clientIp || !clientIp.includes(lastLoginIp))) { - return { code: enumLoginCode.EXPIRES, msg: '登录IP发生变化, 需重新登录' } // IP与上次登录访问的不一致 - } + if (Date.now() > (exp * 1000)) return { code: -1, msg: 'token expires' } // 过期 return { code: enumLoginCode.SUCCESS, msg: 'success' } // 验证成功 } catch (error) { return { code: enumLoginCode.ERROR_TOKEN, msg: error } // token错误, 验证失败 @@ -37,6 +29,6 @@ const isProd = () => { } module.exports = { - verifyAuth, + verifyAuthSync, isProd } \ No newline at end of file diff --git a/server/package.json b/server/package.json index 5985046..57c56fd 100644 --- a/server/package.json +++ b/server/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "@koa/cors": "^5.0.0", + "@seald-io/nedb": "^4.0.4", "axios": "^0.21.4", "consola": "^2.15.3", "cross-env": "^7.0.3", diff --git a/yarn.lock b/yarn.lock index 5528825..cd0e9d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -93,6 +93,20 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@seald-io/binary-search-tree@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@seald-io/binary-search-tree/-/binary-search-tree-1.0.3.tgz#165a9a456eaa30d15885b25db83861bcce2c6a74" + integrity sha512-qv3jnwoakeax2razYaMsGI/luWdliBLHTdC6jU55hQt1hcFqzauH/HsBollQ7IR4ySTtYhT+xyHoijpA16C+tA== + +"@seald-io/nedb@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@seald-io/nedb/-/nedb-4.0.4.tgz#a6f5dd63a2dde0e141f1862da1e0806141791732" + integrity sha512-CUNcMio7QUHTA+sIJ/DC5JzVNNsHe743TPmC4H5Gij9zDLMbmrCT2li3eVB72/gF63BPS8pWEZrjlAMRKA8FDw== + dependencies: + "@seald-io/binary-search-tree" "^1.0.3" + localforage "^1.9.0" + util "^0.12.4" + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -281,6 +295,13 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + axios@^0.21.4: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" @@ -410,6 +431,17 @@ call-bind@^1.0.0: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-bind@^1.0.2, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -714,6 +746,15 @@ defer-to-connect@^1.0.1: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + define-properties@^1.1.3: version "1.1.4" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" @@ -851,6 +892,18 @@ err-code@^2.0.2: resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -1072,6 +1125,13 @@ follow-redirects@^1.14.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.0.tgz#06441868281c86d0dda4ad8bdaead2d02dca89d4" integrity sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ== +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + formidable@^1.1.1: version "1.2.6" resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168" @@ -1129,6 +1189,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" @@ -1162,6 +1227,17 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-stream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -1234,6 +1310,13 @@ globby@^11.0.4: merge2 "^1.4.1" slash "^3.0.0" +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + got@^9.6.0: version "9.6.0" resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" @@ -1273,7 +1356,19 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" -has-symbols@^1.0.1, has-symbols@^1.0.2: +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== @@ -1285,6 +1380,13 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -1302,6 +1404,13 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + http-assert@^1.3.0: version "1.5.0" resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.5.0.tgz#c389ccd87ac16ed2dfa6246fd73b926aa00e6b8f" @@ -1382,6 +1491,11 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -1446,6 +1560,14 @@ into-stream@^6.0.0: from2 "^2.3.0" p-is-promise "^3.0.0" +is-arguments@^1.0.4: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -1453,6 +1575,11 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" +is-callable@^1.1.3: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + is-ci@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" @@ -1534,6 +1661,13 @@ is-path-inside@^3.0.2: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-typed-array@^1.1.3: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" + is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -1801,6 +1935,20 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lie@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" + integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw== + dependencies: + immediate "~3.0.5" + +localforage@^1.9.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" + integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== + dependencies: + lie "3.1.1" + lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" @@ -2268,6 +2416,11 @@ pkg@5.6: stream-meter "^1.0.4" tslib "2.3.1" +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + prebuild-install@6.1.4: version "6.1.4" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.4.tgz#ae3c0142ad611d58570b89af4986088a4937e00f" @@ -2549,6 +2702,18 @@ set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" @@ -3002,6 +3167,17 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +util@^0.12.4: + version "0.12.5" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" + integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + is-typed-array "^1.1.3" + which-typed-array "^1.1.2" + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -3025,6 +3201,17 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" +which-typed-array@^1.1.14, which-typed-array@^1.1.2: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"