update db

This commit is contained in:
chaoszhu 2024-07-10 13:21:47 +08:00
parent 873e20cbcf
commit b8e08666a6
41 changed files with 990 additions and 426 deletions

6
.gitignore vendored
View File

@ -4,4 +4,8 @@ dist
easynode-server.zip
server/app/static/upload/*
server/app/socket/temp/*
server/app/logs/*
app/socket/.sftp-cache/*
server/app/logs/*
server/app/db/*
!server/app/db/README.md
plan.md

View File

@ -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

18
Q&A.md
View File

@ -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

View File

@ -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)

View File

@ -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": {

7
server/.gitignore vendored
View File

@ -1,7 +0,0 @@
node_modules
app/static/upload/*
app/socket/temp/*
app/socket/.sftp-cache/*
app/logs/*
!.gitkeep
dist

View File

@ -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 // 是否记录日志
}
}

View File

@ -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: '移除成功' })
}

View File

@ -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)

View File

@ -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()
}

View File

@ -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

View File

@ -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('密码修改提醒', '面板登录密码已更改')

154
server/app/db.js Normal file
View File

@ -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()
}

View File

@ -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**
> 服务器分组配置

View File

@ -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 = []
}

View File

@ -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()

View File

@ -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()

View File

@ -1,3 +1,4 @@
// 响应压缩模块,自适应头部压缩方式
const compress = require('koa-compress')
const options = { threshold: 2048 }

View File

@ -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',

View File

@ -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

View File

@ -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({

View File

@ -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]

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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": [
]
}

View File

@ -1,7 +0,0 @@
[
{
"id": "default",
"name": "默认分组",
"index": 0
}
]

View File

@ -1 +0,0 @@
[]

View File

@ -1,6 +0,0 @@
{
"pwd": "admin",
"commonKey": "",
"publicKey": "",
"privateKey": ""
}

View File

@ -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
}
]

View File

@ -1 +0,0 @@
[]

View File

@ -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
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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,

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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",

189
yarn.lock
View File

@ -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"