v1.1.0

This commit is contained in:
chaos-zhu 2022-06-27 12:10:38 +08:00
parent d55f791310
commit 19c28ed5a7
38 changed files with 7479 additions and 7106 deletions

View File

@ -12,7 +12,7 @@ const getCertificate =() => {
} }
} }
module.exports = { module.exports = {
domain: 'yourDomain', // 域名xxx.com domain: '', // 域名(必须配置, 跨域使用[不配置将所有域名可访问api])
httpPort: 8082, httpPort: 8082,
httpsPort: 8083, httpsPort: 8083,
clientPort: 22022, // 勿更改 clientPort: 22022, // 勿更改

View File

@ -1,7 +1,7 @@
{ {
"pwd": "admin", "pwd": "admin",
"jwtExpires": "1h", "jwtExpires": "1h",
"jwtSecret": "", "commonKey": "",
"publicKey": "", "publicKey": "",
"privateKey": "" "privateKey": ""
} }

View File

@ -1,4 +1,4 @@
const { readHostList, writeHostList } = require('../utils') const { readHostList, writeHostList, readSSHRecord, writeSSHRecord } = require('../utils')
function getHostList({ res }) { function getHostList({ res }) {
const data = readHostList() const data = readHostList()
@ -33,7 +33,14 @@ function removeHost({ res, request }) {
if(hostIdx === -1) return res.fail({ msg: `${ host }不存在` }) if(hostIdx === -1) return res.fail({ msg: `${ host }不存在` })
hostList.splice(hostIdx, 1) hostList.splice(hostIdx, 1)
writeHostList(hostList) writeHostList(hostList)
res.success({ data: `${ host }已移除` }) // 查询是否存在ssh记录
let sshRecord = readSSHRecord()
let sshIdx = sshRecord.findIndex(item => item.host === host)
let flag = sshIdx !== -1
if(flag) sshRecord.splice(sshIdx, 1)
writeSSHRecord(sshRecord)
res.success({ data: `${ host }已移除, ${ flag ? '并移除ssh记录' : '' }` })
} }
function updateHostSort({ res, request }) { function updateHostSort({ res, request }) {

View File

@ -1,6 +0,0 @@
let getOsData = require('../utils/os-data')
module.exports = async ({ res }) => {
let data = await getOsData()
res.success({ data })
}

View File

@ -1,8 +1,11 @@
const { readSSHRecord, writeSSHRecord } = require('../utils') const { readSSHRecord, writeSSHRecord, AESEncrypt } = require('../utils')
const updateSSH = async ({ res, request }) => { const updateSSH = async ({ res, request }) => {
let { body: { host, port, username, type, password, privateKey, command } } = request let { body: { host, port, username, type, password, privateKey, randomKey, command } } = request
let record = { host, port, username, type, password, privateKey, command } 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() let sshRecord = readSSHRecord()
let idx = sshRecord.findIndex(item => item.host === host) let idx = sshRecord.findIndex(item => item.host === host)
if(idx === -1) if(idx === -1)
@ -10,6 +13,7 @@ const updateSSH = async ({ res, request }) => {
else else
sshRecord.splice(idx, 1, record) sshRecord.splice(idx, 1, record)
writeSSHRecord(sshRecord) writeSSHRecord(sshRecord)
console.log('新增凭证:', host)
res.success({ data: '保存成功' }) res.success({ data: '保存成功' })
} }
@ -19,6 +23,7 @@ const removeSSH = async ({ res, request }) => {
let idx = sshRecord.findIndex(item => item.host === host) let idx = sshRecord.findIndex(item => item.host === host)
if(idx === -1) return res.fail({ msg: '凭证不存在' }) if(idx === -1) return res.fail({ msg: '凭证不存在' })
sshRecord.splice(idx, 1) sshRecord.splice(idx, 1)
console.log('移除凭证:', host)
writeSSHRecord(sshRecord) writeSSHRecord(sshRecord)
res.success({ data: '移除成功' }) res.success({ data: '移除成功' })
} }
@ -27,8 +32,9 @@ const existSSH = async ({ res, request }) => {
let { body: { host } } = request let { body: { host } } = request
let sshRecord = readSSHRecord() let sshRecord = readSSHRecord()
let idx = sshRecord.findIndex(item => item.host === host) let idx = sshRecord.findIndex(item => item.host === host)
if(idx === -1) return res.success({ data: false }) console.log('查询凭证:', host)
res.success({ data: true }) if(idx === -1) return res.success({ data: false }) // host不存在
res.success({ data: true }) // 存在
} }
const getCommand = async ({ res, request }) => { const getCommand = async ({ res, request }) => {
@ -36,10 +42,11 @@ const getCommand = async ({ res, request }) => {
if(!host) return res.fail({ data: false, msg: '参数错误' }) if(!host) return res.fail({ data: false, msg: '参数错误' })
let sshRecord = readSSHRecord() let sshRecord = readSSHRecord()
let record = sshRecord.find(item => item.host === host) let record = sshRecord.find(item => item.host === host)
if(!record) return res.fail({ data: false, msg: 'host not found' }) console.log('查询登录后执行的指令:', host)
if(!record) return res.fail({ data: false, msg: 'host not found' }) // host不存在
const { command } = record const { command } = record
if(!command) return res.success({ data: false }) if(!command) return res.success({ data: false }) // command不存在
res.success({ data: command }) res.success({ data: command }) // 存在
} }
module.exports = { module.exports = {

View File

@ -1,5 +1,5 @@
const jwt = require('jsonwebtoken') const jwt = require('jsonwebtoken')
const { readKey, writeKey, decrypt } = require('../utils') const { getNetIPInfo, readKey, writeKey, RSADecrypt, AESEncrypt, SHA1Encrypt } = require('../utils')
const getpublicKey = ({ res }) => { const getpublicKey = ({ res }) => {
let { publicKey: data } = readKey() let { publicKey: data } = readKey()
@ -7,24 +7,46 @@ const getpublicKey = ({ res }) => {
res.success({ data }) res.success({ data })
} }
const generateTokenAndRecordIP = async (clientIp) => {
console.log('密码校验成功, 准备生成token')
let { commonKey, jwtExpires } = readKey()
let token = jwt.sign({ date: Date.now() }, commonKey, { expiresIn: jwtExpires }) // 生成token
token = AESEncrypt(token) // 对称加密token后再传输给前端
console.log('aes对称加密token', token)
// 记录客户端登录IP用于判断是否异地(只保留最近10条)
const localNetIPInfo = await getNetIPInfo(clientIp)
global.loginRecord.unshift(localNetIPInfo)
if(global.loginRecord.length > 10) global.loginRecord = global.loginRecord.slice(0, 10)
return { token, jwtExpires }
}
const login = async ({ res, request }) => { const login = async ({ res, request }) => {
let { body: { ciphertext } } = request let { body: { ciphertext }, ip: clientIp } = request
if(!ciphertext) return res.fail({ msg: '参数错误' }) if(!ciphertext) return res.fail({ msg: '参数错误' })
try { try {
const password = decrypt(ciphertext) console.log('ciphertext', ciphertext)
let { pwd, jwtSecret, jwtExpires } = readKey() let password = RSADecrypt(ciphertext)
let { pwd } = readKey()
if(password === 'admin' && pwd === 'admin') {
const { token, jwtExpires } = await generateTokenAndRecordIP(clientIp)
return res.success({ data: { token, jwtExpires }, msg: '登录成功,请及时修改默认密码' })
}
password = SHA1Encrypt(password)
if(password !== pwd) return res.fail({ msg: '密码错误' }) if(password !== pwd) return res.fail({ msg: '密码错误' })
const token = jwt.sign({ date: Date.now() }, jwtSecret, { expiresIn: jwtExpires }) // 生成token const { token, jwtExpires } = await generateTokenAndRecordIP(clientIp)
res.success({ data: { token, jwtExpires } }) return res.success({ data: { token, jwtExpires }, msg: '登录成功' })
} catch (error) { } catch (error) {
res.fail({ msg: '解密失败' }) console.log('解密失败:', error)
res.fail({ msg: '解密失败, 请查看服务端日志' })
} }
} }
const updatePwd = async ({ res, request }) => { const updatePwd = async ({ res, request }) => {
let { body: { oldPwd, newPwd } } = request let { body: { oldPwd, newPwd } } = request
oldPwd = decrypt(oldPwd) oldPwd = SHA1Encrypt(RSADecrypt(oldPwd))
newPwd = decrypt(newPwd) newPwd = SHA1Encrypt(RSADecrypt(newPwd))
let keyObj = readKey() let keyObj = readKey()
if(oldPwd !== keyObj.pwd) return res.fail({ data: false, msg: '旧密码校验失败' }) if(oldPwd !== keyObj.pwd) return res.fail({ data: false, msg: '旧密码校验失败' })
keyObj.pwd = newPwd keyObj.pwd = newPwd
@ -32,8 +54,13 @@ const updatePwd = async ({ res, request }) => {
res.success({ data: true, msg: 'success' }) res.success({ data: true, msg: 'success' })
} }
const getLoginRecord = async ({ res }) => {
res.success({ data: global.loginRecord, msg: 'success' })
}
module.exports = { module.exports = {
login, login,
getpublicKey, getpublicKey,
updatePwd updatePwd,
getLoginRecord
} }

View File

@ -1,41 +1,49 @@
const { getLocalNetIP, readHostList, writeHostList, readKey, writeKey, randomStr, isProd } = require('./utils') const { getNetIPInfo, readHostList, writeHostList, readKey, writeKey, randomStr, isProd, AESEncrypt } = require('./utils')
const NodeRSA = require('node-rsa') const NodeRSA = require('node-rsa')
const isDev = !isProd()
// 存储本机IP, 供host列表接口调用
async function initIp() { async function initIp() {
if(!isProd()) return console.log('非生产环境不初始化保存本地IP') if(isDev) return console.log('非生产环境不初始化保存本地IP')
const localNetIP = await getLocalNetIP() const localNetIPInfo = await getNetIPInfo()
let vpsList = readHostList() let vpsList = readHostList()
let { ip: localNetIP } = localNetIPInfo
if(vpsList.some(({ host }) => host === localNetIP)) return console.log('本机IP已储存: ', localNetIP) if(vpsList.some(({ host }) => host === localNetIP)) return console.log('本机IP已储存: ', localNetIP)
vpsList.unshift({ name: 'server-side-host', host: localNetIP }) vpsList.unshift({ name: 'server-side-host', host: localNetIP })
writeHostList(vpsList) writeHostList(vpsList)
console.log('首次启动储存本机IP: ', localNetIP) console.log('Task: 生产环境首次启动储存本机IP: ', localNetIP)
} }
// 初始化公私钥, 供登录、保存ssh密钥/密码等加解密
async function initRsa() { async function initRsa() {
let keyObj = readKey() let keyObj = readKey()
if(keyObj.privateKey && keyObj.publicKey) return console.log('公私钥已存在') if(keyObj.privateKey && keyObj.publicKey) return console.log('公私钥已存在[重新生成会导致已保存的ssh密钥信息失效]')
let key = new NodeRSA({ b: 1024 }) let key = new NodeRSA({ b: 1024 })
key.setOptions({ encryptionScheme: 'pkcs1' }) key.setOptions({ encryptionScheme: 'pkcs1' })
let privateKey = key.exportKey('pkcs1-private-pem') let privateKey = key.exportKey('pkcs1-private-pem')
let publicKey = key.exportKey('pkcs8-public-pem') let publicKey = key.exportKey('pkcs8-public-pem')
keyObj.privateKey = privateKey keyObj.privateKey = AESEncrypt(privateKey) // 加密私钥
keyObj.publicKey = publicKey keyObj.publicKey = publicKey // 公开公钥
writeKey(keyObj) writeKey(keyObj)
console.log('新的公私钥已生成') console.log('Task: 已生成新的非对称加密公私钥')
} }
// 随机的commonKey secret
function randomJWTSecret() { function randomJWTSecret() {
let keyObj = readKey() let keyObj = readKey()
if(keyObj.jwtSecret) return console.log('jwt secret已存在') if(keyObj.commonKey) return console.log('commonKey密钥已存在')
keyObj.jwtSecret = randomStr(32) keyObj.commonKey = randomStr(16)
writeKey(keyObj) writeKey(keyObj)
console.log('已生成随机jwt secret') console.log('Task: 已生成新的随机commonKey密钥')
} }
module.exports = () => { module.exports = () => {
randomJWTSecret() // 先生成全局唯一密钥
initIp() initIp()
initRsa() initRsa()
randomJWTSecret() // 用于记录客户端登录IP的列表
global.loginRecord = []
} }

View File

@ -1,16 +1,20 @@
const { verifyToken } = require('../utils') const { verifyAuth } = require('../utils')
const { apiPrefix } = require('../config') const { apiPrefix } = require('../config')
let whitePath = [ let whitePath = [
'/login', '/login',
'/get-pub-pem' '/get-pub-pem'
].map(item => (apiPrefix + item)) ].map(item => (apiPrefix + item))
console.log('路由白名单:', whitePath)
const useJwt = async ({ request, res }, next) => { const useAuth = async ({ request, res }, next) => {
const { path, headers: { token } } = request const { path, headers: { token } } = request
console.log('path: ', path)
// console.log('token: ', token)
if(whitePath.includes(path)) return next() if(whitePath.includes(path)) return next()
if(!token) return res.fail({ msg: '未登录', status: 403 }) if(!token) return res.fail({ msg: '未登录', status: 403 })
const { code, msg } = verifyToken(token) // 验证token
const { code, msg } = verifyAuth(token, request.ip)
switch(code) { switch(code) {
case 1: case 1:
return await next() return await next()
@ -21,4 +25,4 @@ const useJwt = async ({ request, res }, next) => {
} }
} }
module.exports = useJwt module.exports = useAuth

View File

@ -2,11 +2,15 @@ const koaBody = require('koa-body')
const { uploadDir } = require('../config') const { uploadDir } = require('../config')
module.exports = koaBody({ module.exports = koaBody({
multipart: true, multipart: true, // 支持 multipart-formdate 的表单
formidable: { formidable: {
uploadDir, uploadDir, // 上传目录
keepExtensions: true, keepExtensions: true, // 保持文件的后缀
multipart: true, multipart: true, // 多文件上传
maxFieldsSize: 2 * 1024 * 1024 maxFieldsSize: 2 * 1024 * 1024, // 文件上传大小 单位B
onFileBegin: (name, file) => { // 文件上传前的设置
// console.log(`name: ${name}`)
// console.log(file)
}
} }
}) })

View File

@ -1,11 +1,10 @@
const cors = require('@koa/cors') const cors = require('@koa/cors')
// const { domain } = require('../config') const { domain } = require('../config')
// 跨域处理
const useCors = cors({ const useCors = cors({
origin: ({ req }) => { origin: ({ req }) => {
// console.log(req.headers.origin) return domain || req.headers.origin
// return domain || req.headers.origin
return req.headers.origin
}, },
credentials: true, credentials: true,
allowMethods: [ 'GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH' ] allowMethods: [ 'GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH' ]

View File

@ -1,22 +1,23 @@
const responseHandler = require('./response') const responseHandler = require('./response') // 统一返回格式, 错误捕获
const useJwt = require('./jwt') const useAuth = require('./auth') // 鉴权
const useCors = require('./cors') const useCors = require('./cors') // 处理跨域
const useLog = require('./log4') const useLog = require('./log4') // 记录日志,需要等待路由处理完成,所以得放路由前
const useKoaBody = require('./body') const useKoaBody = require('./body') // 处理body参数 【请求需先走该中间件】
const { useRoutes, useAllowedMethods } = require('./router') const { useRoutes, useAllowedMethods } = require('./router') // 路由管理
const useStatic = require('./static') const useStatic = require('./static') // 静态目录
const compress = require('./compress') const compress = require('./compress') // br/gzip压缩
const history = require('./history') const history = require('./history') // vue-router的history模式
// 注意注册顺序
module.exports = [ module.exports = [
compress, compress,
history, history,
useStatic, useStatic, // staic先注册不然会被jwt拦截
useCors, useCors,
responseHandler, responseHandler,
useKoaBody, useKoaBody, // 先处理bodylog和router都要用到
useLog, useLog, // 日志记录开始【该module使用到了fs.mkdir()等读写api 设置保存日志的目录需使用process.cwd()】
useJwt, useAuth,
useAllowedMethods, useAllowedMethods,
useRoutes useRoutes
] ]

View File

@ -3,12 +3,14 @@ const { outDir, flag } = require('../config').logConfig
log4js.configure({ log4js.configure({
appenders: { appenders: {
// 控制台输出
out: { out: {
type: 'stdout', type: 'stdout',
layout: { layout: {
type: 'colored' type: 'colored'
} }
}, },
// 保存日志文件
cheese: { cheese: {
type: 'file', type: 'file',
maxLogSize: 512*1024, // unit: bytes 1KB = 1024bytes maxLogSize: 512*1024, // unit: bytes 1KB = 1024bytes
@ -17,10 +19,11 @@ log4js.configure({
}, },
categories: { categories: {
default: { default: {
appenders: [ 'out', 'cheese' ], appenders: [ 'out', 'cheese' ], // 配置
level: 'info' level: 'info' // 只输出info以上级别的日志
} }
} }
// pm2: true
}) })
const logger = log4js.getLogger() const logger = log4js.getLogger()
@ -37,7 +40,8 @@ const useLog = () => {
ip, ip,
headers headers
} }
await next() await next() // 等待路由处理完成,再开始记录日志
// 是否记录日志
if (flag) { if (flag) {
const { status, params } = ctx const { status, params } = ctx
data.status = status data.status = status

View File

@ -1,30 +1,32 @@
const responseHandler = async (ctx, next) => { const responseHandler = async (ctx, next) => {
// 统一成功响应
ctx.res.success = ({ status, data, msg = 'success' } = {}) => { ctx.res.success = ({ status, data, msg = 'success' } = {}) => {
ctx.status = status || 200 ctx.status = status || 200 // 没传默认200
ctx.body = { ctx.body = {
status: ctx.status, status: ctx.status, // 响应成功默认 200
data, data,
msg msg
} }
} }
// 统一错误响应
ctx.res.fail = ({ status, msg = 'fail', data = {} } = {}) => { ctx.res.fail = ({ status, msg = 'fail', data = {} } = {}) => {
ctx.status = status || 400 ctx.status = status || 400 // 响应失败默认 400
ctx.body = { ctx.body = {
status, status, // 失败默认 400
data, data,
msg msg
} }
} }
// 错误响应捕获
try { try {
await next() await next() // 每个中间件都需等待next完成调用不然会返回404给前端!!!
} catch (err) { } catch (err) {
console.log('中间件错误:', err) console.log('中间件错误:', err)
if (err.status) if (err.status)
ctx.res.fail({ status: err.status, msg: err.message }) ctx.res.fail({ status: err.status, msg: err.message }) // 自己主动抛出的错误 throwError
else else
ctx.app.emit('error', err, ctx) ctx.app.emit('error', err, ctx) // 程序运行时的错误 main.js中监听
} }
} }

View File

@ -1,6 +1,10 @@
const router = require('../router') const router = require('../router')
// 路由中间件
const useRoutes = router.routes() const useRoutes = router.routes()
// 优化错误提示中间件
// 原先如果请求方法错误响应404
// 使用该中间件后请求方法错误会提示405 Method Not Allowed【get list ✔200 post /list ❌405】
const useAllowedMethods = router.allowedMethods() const useAllowedMethods = router.allowedMethods()
module.exports = { module.exports = {

View File

@ -4,6 +4,7 @@ const router = new koaRouter({ prefix: apiPrefix })
const routeList = require('./routes') const routeList = require('./routes')
// 统一注册路由
routeList.forEach(item => { routeList.forEach(item => {
const { method, path, controller } = item const { method, path, controller } = item
router[method](path, controller) router[method](path, controller)

View File

@ -1,14 +1,9 @@
const osInfo = require('../controller/os-info')
const { updateSSH, removeSSH, existSSH, getCommand } = require('../controller/ssh-info') const { updateSSH, removeSSH, existSSH, getCommand } = require('../controller/ssh-info')
const { getHostList, saveHost, updateHost, removeHost, updateHostSort } = require('../controller/host-info') const { getHostList, saveHost, updateHost, removeHost, updateHostSort } = require('../controller/host-info')
const { login, getpublicKey, updatePwd } = require('../controller/user') const { login, getpublicKey, updatePwd, getLoginRecord } = require('../controller/user')
// 路由统一管理
const routes = [ const routes = [
{
method: 'get',
path: '/os-info',
controller: osInfo
},
{ {
method: 'post', method: 'post',
path: '/update-ssh', path: '/update-ssh',
@ -68,6 +63,11 @@ const routes = [
method: 'put', method: 'put',
path: '/pwd', path: '/pwd',
controller: updatePwd controller: updatePwd
},
{
method: 'get',
path: '/get-login-record',
controller: getLoginRecord
} }
] ]

View File

@ -1,5 +1,5 @@
const Koa = require('koa') const Koa = require('koa')
const compose = require('koa-compose') const compose = require('koa-compose') // 组合中间件,简化写法
const http = require('http') const http = require('http')
const https = require('https') const https = require('https')
const { clientPort } = require('./config') const { clientPort } = require('./config')
@ -7,14 +7,16 @@ const { domain, httpPort, httpsPort, certificate } = require('./config')
const middlewares = require('./middlewares') const middlewares = require('./middlewares')
const wsMonitorOsInfo = require('./socket/monitor') const wsMonitorOsInfo = require('./socket/monitor')
const wsTerminal = require('./socket/terminal') const wsTerminal = require('./socket/terminal')
const wsHostStatus = require('./socket/host-status')
const wsClientInfo = require('./socket/clients') const wsClientInfo = require('./socket/clients')
const { throwError } = require('./utils') const { throwError } = require('./utils')
const httpServer = () => { const httpServer = () => {
// if(EXEC_ENV === 'production') return console.log('========生成环境不创建http服务========')
const app = new Koa() const app = new Koa()
const server = http.createServer(app.callback()) const server = http.createServer(app.callback())
serverHandler(app, server) serverHandler(app, server)
// ws一直报跨域的错误参照官方文档使用createServer API创建服务
server.listen(httpPort, () => { server.listen(httpPort, () => {
console.log(`Server(http) is running on: http://localhost:${ httpPort }`) console.log(`Server(http) is running on: http://localhost:${ httpPort }`)
}) })
@ -34,17 +36,21 @@ const httpsServer = () => {
const clientHttpServer = () => { const clientHttpServer = () => {
const app = new Koa() const app = new Koa()
const server = http.createServer(app.callback()) const server = http.createServer(app.callback())
wsMonitorOsInfo(server) wsMonitorOsInfo(server) // 监控本机信息
server.listen(clientPort, () => { server.listen(clientPort, () => {
console.log(`Client(http) is running on: http://localhost:${ clientPort }`) console.log(`Client(http) is running on: http://localhost:${ clientPort }`)
}) })
} }
// 服务
function serverHandler(app, server) { function serverHandler(app, server) {
wsTerminal(server) app.proxy = true // 用于nginx反代时获取真实客户端ip
wsClientInfo(server) wsTerminal(server) // 终端
app.context.throwError = throwError wsHostStatus(server) // 终端侧边栏host信息
wsClientInfo(server) // 客户端信息
app.context.throwError = throwError // 常用方法挂载全局ctx上
app.use(compose(middlewares)) app.use(compose(middlewares))
// 捕获error.js模块抛出的服务错误
app.on('error', (err, ctx) => { app.on('error', (err, ctx) => {
ctx.status = 500 ctx.status = 500
ctx.body = { ctx.body = {

View File

@ -2,9 +2,9 @@ const { Server: ServerIO } = require('socket.io')
const { io: ClientIO } = require('socket.io-client') const { io: ClientIO } = require('socket.io-client')
const { readHostList } = require('../utils') const { readHostList } = require('../utils')
const { clientPort } = require('../config') const { clientPort } = require('../config')
const { verifyToken } = require('../utils') const { verifyAuth } = require('../utils')
let clientSockets = {}, clientsData = {}, timer = null let clientSockets = {}, clientsData = {}
function getClientsInfo(socketId) { function getClientsInfo(socketId) {
let hostList = readHostList() let hostList = readHostList()
@ -13,9 +13,11 @@ function getClientsInfo(socketId) {
let clientSocket = ClientIO(`http://${ host }:${ clientPort }`, { let clientSocket = ClientIO(`http://${ host }:${ clientPort }`, {
path: '/client/os-info', path: '/client/os-info',
forceNew: true, forceNew: true,
timeout: 5000,
reconnectionDelay: 3000, reconnectionDelay: 3000,
reconnectionAttempts: 1 reconnectionAttempts: 100
}) })
// 将与客户端连接的socket实例保存起来web端断开时关闭这些连接
clientSockets[socketId].push(clientSocket) clientSockets[socketId].push(clientSocket)
return { return {
host, host,
@ -25,6 +27,7 @@ function getClientsInfo(socketId) {
.map(({ host, clientSocket }) => { .map(({ host, clientSocket }) => {
clientSocket clientSocket
.on('connect', () => { .on('connect', () => {
console.log('client connect success:', host)
clientSocket.on('client_data', (osData) => { clientSocket.on('client_data', (osData) => {
clientsData[host] = osData clientsData[host] = osData
}) })
@ -32,10 +35,12 @@ function getClientsInfo(socketId) {
clientsData[host] = error clientsData[host] = error
}) })
}) })
.on('connect_error', () => { .on('connect_error', (error) => {
console.log('client connect fail:', host, error.message)
clientsData[host] = null clientsData[host] = null
}) })
.on('disconnect', () => { .on('disconnect', () => {
console.log('client connect disconnect:', host)
clientsData[host] = null clientsData[host] = null
}) })
}) })
@ -45,28 +50,46 @@ module.exports = (httpServer) => {
const serverIo = new ServerIO(httpServer, { const serverIo = new ServerIO(httpServer, {
path: '/clients', path: '/clients',
cors: { cors: {
origin: '*' // 需配置跨域
} }
}) })
serverIo.on('connection', (socket) => { 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', ({ token }) => {
const { code } = verifyToken(token) // 校验登录态
if(code !== 1) return socket.emit('token_verify_fail', 'token无效') const { code, msg } = verifyAuth(token, clientIp)
if(code !== 1) {
socket.emit('token_verify_fail', msg || '鉴权失败')
socket.disconnect()
return
}
// 收集web端连接的id
clientSockets[socket.id] = [] clientSockets[socket.id] = []
console.log('client连接socketId: ', socket.id, 'clients-socket已连接数: ', Object.keys(clientSockets).length)
// 获取客户端数据
getClientsInfo(socket.id) getClientsInfo(socket.id)
// 立即推送一次
socket.emit('clients_data', clientsData) socket.emit('clients_data', clientsData)
// 向web端推送数据
let timer = null
timer = setInterval(() => { timer = setInterval(() => {
socket.emit('clients_data', clientsData) socket.emit('clients_data', clientsData)
}, 1500) }, 1000)
// 关闭连接
socket.on('disconnect', () => { socket.on('disconnect', () => {
// 防止内存泄漏
if(timer) clearInterval(timer) if(timer) clearInterval(timer)
// 当web端与服务端断开连接时, 服务端与每个客户端的socket也应该断开连接
clientSockets[socket.id].forEach(socket => socket.close && socket.close()) clientSockets[socket.id].forEach(socket => socket.close && socket.close())
delete clientSockets[socket.id] delete clientSockets[socket.id]
console.log('断开socketId: ', socket.id, 'clients-socket剩余连接数: ', Object.keys(clientSockets).length)
}) })
}) })
}) })

View File

@ -0,0 +1,74 @@
const { Server: ServerIO } = require('socket.io')
const { io: ClientIO } = require('socket.io-client')
const { clientPort } = require('../config')
const { verifyAuth } = require('../utils')
let hostSockets = {}
function getHostInfo(serverSocket, host) {
let hostSocket = ClientIO(`http://${ host }:${ clientPort }`, {
path: '/client/os-info',
forceNew: true,
timeout: 5000,
reconnectionDelay: 3000,
reconnectionAttempts: 100
})
// 将与客户端连接的socket实例保存起来web端断开时关闭与客户端的连接
hostSockets[serverSocket.id] = hostSocket
hostSocket
.on('connect', () => {
console.log('客户端状态socket连接成功:', host)
hostSocket.on('client_data', (data) => {
serverSocket.emit('host_data', data)
})
hostSocket.on('client_error', () => {
serverSocket.emit('host_data', null)
})
})
.on('connect_error', (error) => {
console.log('客户端状态socket连接[失败]:', host, error.message)
serverSocket.emit('host_data', null)
})
.on('disconnect', () => {
console.log('客户端状态socket连接[断开]:', host)
serverSocket.emit('host_data', null)
})
}
module.exports = (httpServer) => {
const serverIo = new ServerIO(httpServer, {
path: '/host-status',
cors: {
origin: '*' // 需配置跨域
}
})
serverIo.on('connection', (serverSocket) => {
// 前者兼容nginx反代, 后者兼容nodejs自身服务
let clientIp = serverSocket.handshake.headers['x-forwarded-for'] || serverSocket.handshake.address
serverSocket.on('init_host_data', ({ token, host }) => {
// 校验登录态
const { code, msg } = verifyAuth(token, clientIp)
if(code !== 1) {
serverSocket.emit('token_verify_fail', msg || '鉴权失败')
serverSocket.disconnect()
return
}
// 获取客户端数据
getHostInfo(serverSocket, host)
console.log('host-socket连接socketId: ', serverSocket.id, 'host-socket已连接数: ', Object.keys(hostSockets).length)
// 关闭连接
serverSocket.on('disconnect', () => {
// 当web端与服务端断开连接时, 服务端与每个客户端的socket也应该断开连接
let socket = hostSockets[serverSocket.id]
socket.close && socket.close()
delete hostSockets[serverSocket.id]
console.log('host-socket剩余连接数: ', Object.keys(hostSockets).length)
})
})
})
}

View File

@ -25,6 +25,7 @@ function ipSchedule() {
getIpInfo() getIpInfo()
}) })
// 每日凌晨两点整,刷新ip信息(兼容动态ip服务器)
let rule2 = new schedule.RecurrenceRule() let rule2 = new schedule.RecurrenceRule()
rule2.hour = 2 rule2.hour = 2
rule2.minute = 0 rule2.minute = 0
@ -46,6 +47,7 @@ module.exports = (httpServer) => {
}) })
serverIo.on('connection', (socket) => { serverIo.on('connection', (socket) => {
// 存储对应websocket连接的定时器
serverSockets[socket.id] = setInterval(async () => { serverSockets[socket.id] = setInterval(async () => {
try { try {
osData = await getOsData() osData = await getOsData()
@ -54,13 +56,15 @@ module.exports = (httpServer) => {
console.error('客户端错误:', error) console.error('客户端错误:', error)
socket && socket.emit('client_error', { error }) socket && socket.emit('client_error', { error })
} }
}, 1500) }, 1000)
socket.on('disconnect', () => { socket.on('disconnect', () => {
// 断开时清楚对应的websocket连接
if(serverSockets[socket.id]) clearInterval(serverSockets[socket.id]) if(serverSockets[socket.id]) clearInterval(serverSockets[socket.id])
delete serverSockets[socket.id] delete serverSockets[socket.id]
socket.close && socket.close() socket.close && socket.close()
socket = null socket = null
// console.log('断开socketId: ', socket.id, '剩余链接数: ', Object.keys(serverSockets).length)
}) })
}) })
} }

View File

@ -1,15 +1,16 @@
const { Server } = require('socket.io') const { Server } = require('socket.io')
const { Client: Client } = require('ssh2') const { Client: Client } = require('ssh2')
const { readSSHRecord, verifyToken } = require('../utils') const { readSSHRecord, verifyAuth, RSADecrypt, AESDecrypt } = require('../utils')
function createTerminal(socket, vps) { function createTerminal(socket, vps) {
vps.shell({ term: 'xterm-color', cols: 100, rows: 30 }, (err, stream) => { vps.shell({ term: 'xterm-color' }, (err, stream) => {
if (err) return socket.emit('output', err.toString()) if (err) return socket.emit('output', err.toString())
stream stream
.on('data', (data) => { .on('data', (data) => {
socket.emit('output', data.toString()) socket.emit('output', data.toString())
}) })
.on('close', () => { .on('close', () => {
console.log('关闭终端')
vps.end() vps.end()
}) })
socket.on('input', key => { socket.on('input', key => {
@ -18,6 +19,9 @@ function createTerminal(socket, vps) {
}) })
socket.emit('connect_terminal') socket.emit('connect_terminal')
socket.on('resize', ({ rows, cols }) => {
stream.setWindow(rows, cols)
})
}) })
} }
@ -29,24 +33,37 @@ module.exports = (httpServer) => {
} }
}) })
serverIo.on('connection', (socket) => { serverIo.on('connection', (socket) => {
// 前者兼容nginx反代, 后者兼容nodejs自身服务
let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
let vps = new Client() let vps = new Client()
console.log('terminal websocket 已连接')
socket.on('create', ({ host: ip, token }) => { socket.on('create', ({ host: ip, token }) => {
const { code } = verifyAuth(token, clientIp)
const { code } = verifyToken(token) if(code !== 1) {
if(code !== 1) return socket.emit('token_verify_fail') socket.emit('token_verify_fail')
socket.disconnect()
return
}
// console.log('code:', code)
const sshRecord = readSSHRecord() const sshRecord = readSSHRecord()
let loginInfo = sshRecord.find(item => item.host === ip) 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 }】凭证`)
const { type, host, port, username } = loginInfo let { type, host, port, username, randomKey } = loginInfo
try { try {
// 解密放到try里面防止报错【公私钥必须配对, 否则需要重新添加服务器密钥】
randomKey = AESDecrypt(randomKey) // 先对称解密key
randomKey = RSADecrypt(randomKey) // 再非对称解密key
loginInfo[type] = AESDecrypt(loginInfo[type], randomKey) // 对称解密ssh密钥
console.log('准备连接服务器:', host)
vps vps
.on('ready', () => { .on('ready', () => {
console.log('已连接到服务器:', host)
socket.emit('connect_success', `已连接到服务器:${ host }`) socket.emit('connect_success', `已连接到服务器:${ host }`)
createTerminal(socket, vps) createTerminal(socket, vps)
}) })
.on('error', (err) => { .on('error', (err) => {
console.log('连接失败:', err.level)
socket.emit('connect_fail', err.message) socket.emit('connect_fail', err.message)
}) })
.connect({ .connect({
@ -55,14 +72,18 @@ module.exports = (httpServer) => {
port, port,
username, username,
[type]: loginInfo[type] [type]: loginInfo[type]
// debug: (info) => {
// console.log(info)
// }
}) })
} catch (err) { } catch (err) {
console.log('创建失败:', err.message)
socket.emit('create_fail', err.message) socket.emit('create_fail', err.message)
} }
}) })
socket.on('disconnect', () => { socket.on('disconnect', (reason) => {
console.log('终端连接断开:', reason)
vps.end() vps.end()
vps.destroy() vps.destroy()
vps = null vps = null

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,11 +5,11 @@
<meta name="viewport" content="width=device-width, initial-scale=0,user-scalable=yes"> <meta name="viewport" content="width=device-width, initial-scale=0,user-scalable=yes">
<title>EasyNode</title> <title>EasyNode</title>
<script src="//at.alicdn.com/t/font_3309550_eg9tjmfmiku.js"></script> <script src="//at.alicdn.com/t/font_3309550_eg9tjmfmiku.js"></script>
<script type="module" crossorigin src="/assets/index.49bfeae7.js"></script> <script type="module" crossorigin src="/assets/index.704cb447.js"></script>
<link rel="stylesheet" href="/assets/index.4226ec12.css"> <link rel="stylesheet" href="/assets/index.fdac59aa.css">
</head> </head>
<body> <body>
<div id="app" style="opacity: 0.9;"></div> <div id="app"></div>
</body> </body>
</html> </html>

View File

@ -1,7 +1,10 @@
const fs = require('fs') const fs = require('fs')
const CryptoJS = require('crypto-js')
const rawCrypto = require('crypto')
const NodeRSA = require('node-rsa')
const jwt = require('jsonwebtoken') const jwt = require('jsonwebtoken')
const axios = require('axios') const axios = require('axios')
const NodeRSA = require('node-rsa') const request = axios.create({ timeout: 3000 })
const { sshRecordPath, hostListPath, keyPath } = require('../config') const { sshRecordPath, hostListPath, keyPath } = require('../config')
@ -10,6 +13,7 @@ const readSSHRecord = () => {
try { try {
list = JSON.parse(fs.readFileSync(sshRecordPath, 'utf8')) list = JSON.parse(fs.readFileSync(sshRecordPath, 'utf8'))
} catch (error) { } catch (error) {
console.log('读取ssh-record错误, 即将重置ssh列表: ', error)
writeSSHRecord([]) writeSSHRecord([])
} }
return list || [] return list || []
@ -24,6 +28,7 @@ const readHostList = () => {
try { try {
list = JSON.parse(fs.readFileSync(hostListPath, 'utf8')) list = JSON.parse(fs.readFileSync(hostListPath, 'utf8'))
} catch (error) { } catch (error) {
console.log('读取host-list错误, 即将重置host列表: ', error)
writeHostList([]) writeHostList([])
} }
return list || [] return list || []
@ -42,11 +47,24 @@ const writeKey = (keyObj = {}) => {
fs.writeFileSync(keyPath, JSON.stringify(keyObj, null, 2)) fs.writeFileSync(keyPath, JSON.stringify(keyObj, null, 2))
} }
const getLocalNetIP = async () => { // 为空时请求本地IP
const getNetIPInfo = async (ip = '') => {
try { try {
let ipUrls = ['http://ip-api.com/json/?lang=zh-CN', 'http://whois.pconline.com.cn/ipJson.jsp?json=true'] let date = getUTCDate(8)
let { data } = await Promise.race(ipUrls.map(url => axios.get(url))) let ipUrls = [`http://ip-api.com/json/${ ip }?lang=zh-CN`, `http://whois.pconline.com.cn/ipJson.jsp?json=true&ip=${ ip }`]
return data.ip || data.query let result = await Promise.allSettled(ipUrls.map(url => request.get(url)))
let [ipApi, pconline] = result
if(ipApi.status === 'fulfilled') {
let { query: ip, country, regionName, city } = ipApi.value.data
// console.log({ ip, country, city: regionName + city })
return { ip, country, city: regionName + city, date }
}
if(pconline.status === 'fulfilled') {
let { ip, pro, city, addr } = pconline.value.data
// console.log({ ip, country: pro || addr, city })
return { ip, country: pro || addr, city, date }
}
throw Error('获取IP信息API出错,请排查或更新API')
} catch (error) { } catch (error) {
console.error('getIpInfo Error: ', error) console.error('getIpInfo Error: ', error)
return { return {
@ -60,7 +78,7 @@ const getLocalNetIP = async () => {
const throwError = ({ status = 500, msg = 'defalut error' } = {}) => { const throwError = ({ status = 500, msg = 'defalut error' } = {}) => {
const err = new Error(msg) const err = new Error(msg)
err.status = status err.status = status // 主动抛错
throw err throw err
} }
@ -71,7 +89,7 @@ const isIP = (ip = '') => {
} }
const randomStr = (e) =>{ const randomStr = (e) =>{
e = e || 32 e = e || 16
let str = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678', let str = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678',
a = str.length, a = str.length,
res = '' res = ''
@ -79,14 +97,27 @@ const randomStr = (e) =>{
return res return res
} }
const verifyToken = (token) =>{ // 校验token与登录IP
const { jwtSecret } = readKey() const verifyAuth = (token, clientIp) =>{
token = AESDecrypt(token) // 先aes解密
const { commonKey } = readKey()
try { try {
const { exp } = jwt.verify(token, jwtSecret) const { exp } = jwt.verify(token, commonKey)
if(Date.now() > (exp * 1000)) return { code: -1, msg: 'token expires' } // console.log('校验token', new Date(), '---', new Date(exp * 1000))
return { code: 1, msg: 'success' } if(Date.now() > (exp * 1000)) return { code: -1, msg: 'token expires' } // 过期
let lastLoginIp = global.loginRecord[0] ? global.loginRecord[0].ip : ''
console.log('校验客户端IP', clientIp)
console.log('最后登录的IP', lastLoginIp)
// 判断: (生产环境)clientIp与上次登录成功IP不一致
if(isProd() && (!lastLoginIp || !clientIp || !clientIp.includes(lastLoginIp))) {
return { code: -1, msg: '登录IP发生变化, 需重新登录' } // IP与上次登录访问的不一致
}
// console.log('token验证成功')
return { code: 1, msg: 'success' } // 验证成功
} catch (error) { } catch (error) {
return { code: -2, msg: error } // console.log('token校验错误', error)
return { code: -2, msg: error } // token错误, 验证失败
} }
} }
@ -95,26 +126,64 @@ const isProd = () => {
return EXEC_ENV === 'production' return EXEC_ENV === 'production'
} }
const decrypt = (ciphertext) => { // rsa非对称 私钥解密
const RSADecrypt = (ciphertext) => {
if(!ciphertext) return
let { privateKey } = readKey() let { privateKey } = readKey()
privateKey = AESDecrypt(privateKey) // 先解密私钥
const rsakey = new NodeRSA(privateKey) const rsakey = new NodeRSA(privateKey)
rsakey.setOptions({ encryptionScheme: 'pkcs1' }) rsakey.setOptions({ encryptionScheme: 'pkcs1' }) // Must Set It When Frontend Use jsencrypt
const plaintext = rsakey.decrypt(ciphertext, 'utf8') const plaintext = rsakey.decrypt(ciphertext, 'utf8')
return plaintext return plaintext
} }
// aes对称 加密(default commonKey)
const AESEncrypt = (text, key) => {
if(!text) return
let { commonKey } = readKey()
let ciphertext = CryptoJS.AES.encrypt(text, key || commonKey).toString()
return ciphertext
}
// aes对称 解密(default commonKey)
const AESDecrypt = (ciphertext, key) => {
if(!ciphertext) return
let { commonKey } = readKey()
let bytes = CryptoJS.AES.decrypt(ciphertext, key || commonKey)
let originalText = bytes.toString(CryptoJS.enc.Utf8)
return originalText
}
// sha1 加密(不可逆)
const SHA1Encrypt = (clearText) => {
return rawCrypto.createHash('sha1').update(clearText).digest('hex')
}
// 获取UTC-x时间
const getUTCDate = (num = 8) => {
let date = new Date()
let now_utc = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(),
date.getUTCDate(), date.getUTCHours() + num,
date.getUTCMinutes(), date.getUTCSeconds())
return new Date(now_utc)
}
module.exports = { module.exports = {
readSSHRecord, readSSHRecord,
writeSSHRecord, writeSSHRecord,
readHostList, readHostList,
writeHostList, writeHostList,
getLocalNetIP, getNetIPInfo,
throwError, throwError,
isIP, isIP,
readKey, readKey,
writeKey, writeKey,
randomStr, randomStr,
verifyToken, verifyAuth,
isProd, isProd,
decrypt RSADecrypt,
AESEncrypt,
AESDecrypt,
SHA1Encrypt,
getUTCDate
} }

View File

@ -9,7 +9,7 @@ let osuOs = osu.os
let users = osu.users let users = osu.users
async function cpuInfo() { async function cpuInfo() {
let cpuUsage = await cpu.usage(300) let cpuUsage = await cpu.usage(200)
let cpuCount = cpu.count() let cpuCount = cpu.count()
let cpuModel = cpu.model() let cpuModel = cpu.model()
return { return {
@ -37,7 +37,7 @@ async function driveInfo() {
} }
async function netstatInfo() { async function netstatInfo() {
let netstatInfo = await netstat.inOut(300) let netstatInfo = await netstat.inOut()
return netstatInfo === 'not supported' ? {} : netstatInfo return netstatInfo === 'not supported' ? {} : netstatInfo
} }

3
server/bin/www Normal file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env node
console.log('start time', new Date())
require('../app/main.js')

View File

@ -1,6 +1,6 @@
{ {
"name": "easynode-server", "name": "easynode-server",
"version": "0.0.1", "version": "1.1.0",
"description": "easynode-server", "description": "easynode-server",
"bin": "./bin/www", "bin": "./bin/www",
"pkg": { "pkg": {
@ -13,7 +13,8 @@
"server": "cross-env EXEC_ENV=production nodemon ./app/main.js", "server": "cross-env EXEC_ENV=production nodemon ./app/main.js",
"start": "pm2 start ./app/main.js", "start": "pm2 start ./app/main.js",
"pkgwin": "pkg . -t node16-win-x64", "pkgwin": "pkg . -t node16-win-x64",
"pkglinux": "pkg . -t node16-linux-x64" "pkglinux:x86": "pkg . -t node16-linux-x64",
"pkglinux:arm": "pkg . -t node16-linux-arm64"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
@ -26,6 +27,8 @@
"dependencies": { "dependencies": {
"@koa/cors": "^3.1.0", "@koa/cors": "^3.1.0",
"axios": "^0.21.4", "axios": "^0.21.4",
"crypto-js": "^4.1.1",
"global": "^4.4.0",
"is-ip": "^4.0.0", "is-ip": "^4.0.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"koa": "^2.13.1", "koa": "^2.13.1",

View File

@ -631,6 +631,11 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.2:
shebang-command "^2.0.0" shebang-command "^2.0.0"
which "^2.0.1" which "^2.0.1"
crypto-js@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf"
integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==
crypto-random-string@^2.0.0: crypto-random-string@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
@ -736,6 +741,11 @@ doctrine@^3.0.0:
dependencies: dependencies:
esutils "^2.0.2" esutils "^2.0.2"
dom-walk@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84"
integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==
dot-prop@^5.2.0: dot-prop@^5.2.0:
version "5.3.0" version "5.3.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"
@ -1172,6 +1182,14 @@ global-dirs@^3.0.0:
dependencies: dependencies:
ini "2.0.0" ini "2.0.0"
global@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406"
integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==
dependencies:
min-document "^2.19.0"
process "^0.11.10"
globals@^13.6.0, globals@^13.9.0: globals@^13.6.0, globals@^13.9.0:
version "13.15.0" version "13.15.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-13.15.0.tgz#38113218c907d2f7e98658af246cef8b77e90bac" resolved "https://registry.yarnpkg.com/globals/-/globals-13.15.0.tgz#38113218c907d2f7e98658af246cef8b77e90bac"
@ -1889,6 +1907,13 @@ mimic-response@^2.0.0:
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43"
integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==
min-document@^2.19.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
integrity sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==
dependencies:
dom-walk "^0.1.0"
minimatch@^3.0.4, minimatch@^3.1.1: minimatch@^3.0.4, minimatch@^3.1.1:
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@ -2243,6 +2268,11 @@ process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
process@^0.11.10:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
progress@^2.0.0, progress@^2.0.3: progress@^2.0.0, progress@^2.0.3:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
@ -2695,6 +2725,11 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
systeminformation@^5.11.16:
version "5.11.16"
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.11.16.tgz#5f6fda2447fafe204bd2ab543475f1ffa8c14a85"
integrity sha512-/a1VfP9WELKLT330yhAHJ4lWCXRYynel1kMMHKc/qdzCgDt3BIcMlo+3tKcTiRHFefjV3fz4AvqMx7dGO/72zw==
table@^6.0.9: table@^6.0.9:
version "6.8.0" version "6.8.0"
resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca" resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca"

View File

@ -631,6 +631,11 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.2:
shebang-command "^2.0.0" shebang-command "^2.0.0"
which "^2.0.1" which "^2.0.1"
crypto-js@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf"
integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==
crypto-random-string@^2.0.0: crypto-random-string@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
@ -736,6 +741,11 @@ doctrine@^3.0.0:
dependencies: dependencies:
esutils "^2.0.2" esutils "^2.0.2"
dom-walk@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84"
integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==
dot-prop@^5.2.0: dot-prop@^5.2.0:
version "5.3.0" version "5.3.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"
@ -1172,6 +1182,14 @@ global-dirs@^3.0.0:
dependencies: dependencies:
ini "2.0.0" ini "2.0.0"
global@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406"
integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==
dependencies:
min-document "^2.19.0"
process "^0.11.10"
globals@^13.6.0, globals@^13.9.0: globals@^13.6.0, globals@^13.9.0:
version "13.15.0" version "13.15.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-13.15.0.tgz#38113218c907d2f7e98658af246cef8b77e90bac" resolved "https://registry.yarnpkg.com/globals/-/globals-13.15.0.tgz#38113218c907d2f7e98658af246cef8b77e90bac"
@ -1889,6 +1907,13 @@ mimic-response@^2.0.0:
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43"
integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==
min-document@^2.19.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
integrity sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==
dependencies:
dom-walk "^0.1.0"
minimatch@^3.0.4, minimatch@^3.1.1: minimatch@^3.0.4, minimatch@^3.1.1:
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@ -2243,6 +2268,11 @@ process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
process@^0.11.10:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
progress@^2.0.0, progress@^2.0.3: progress@^2.0.0, progress@^2.0.3:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
@ -2695,6 +2725,11 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
systeminformation@^5.11.16:
version "5.11.22"
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.11.22.tgz#52eb78fd6bb48eef372f502b494ff59aacf82c02"
integrity sha512-sBZJ/WBCf2vDLeMZaEyVuo+aXylOSmNHHB2cX0jHULFxSBLXHX+QUHYrCvmz+YiflKY3bsahVWX7vwuz1p1QZA==
table@^6.0.9: table@^6.0.9:
version "6.8.0" version "6.8.0"
resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca" resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca"