✨ v1.1.0
This commit is contained in:
parent
d55f791310
commit
19c28ed5a7
@ -1,67 +1,67 @@
|
|||||||
const { Server } = require('socket.io')
|
const { Server } = require('socket.io')
|
||||||
const schedule = require('node-schedule')
|
const schedule = require('node-schedule')
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
let getOsData = require('../utils/os-data')
|
let getOsData = require('../utils/os-data')
|
||||||
|
|
||||||
let serverSockets = {}, ipInfo = {}, osData = {}
|
let serverSockets = {}, ipInfo = {}, osData = {}
|
||||||
|
|
||||||
async function getIpInfo() {
|
async function getIpInfo() {
|
||||||
try {
|
try {
|
||||||
let { data } = await axios.get('http://ip-api.com/json?lang=zh-CN')
|
let { data } = await axios.get('http://ip-api.com/json?lang=zh-CN')
|
||||||
console.log('getIpInfo Success: ', new Date())
|
console.log('getIpInfo Success: ', new Date())
|
||||||
ipInfo = data
|
ipInfo = data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('getIpInfo Error: ', new Date(), error)
|
console.log('getIpInfo Error: ', new Date(), error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ipSchedule() {
|
function ipSchedule() {
|
||||||
let rule1 = new schedule.RecurrenceRule()
|
let rule1 = new schedule.RecurrenceRule()
|
||||||
rule1.second = [0, 10, 20, 30, 40, 50]
|
rule1.second = [0, 10, 20, 30, 40, 50]
|
||||||
schedule.scheduleJob(rule1, () => {
|
schedule.scheduleJob(rule1, () => {
|
||||||
let { query, country, city } = ipInfo || {}
|
let { query, country, city } = ipInfo || {}
|
||||||
if(query && country && city) return
|
if(query && country && city) return
|
||||||
console.log('Task: start getIpInfo', new Date())
|
console.log('Task: start getIpInfo', new Date())
|
||||||
getIpInfo()
|
getIpInfo()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 每日凌晨两点整,刷新ip信息(兼容动态ip服务器)
|
// 每日凌晨两点整,刷新ip信息(兼容动态ip服务器)
|
||||||
let rule2 = new schedule.RecurrenceRule()
|
let rule2 = new schedule.RecurrenceRule()
|
||||||
rule2.hour = 2
|
rule2.hour = 2
|
||||||
rule2.minute = 0
|
rule2.minute = 0
|
||||||
rule2.second = 0
|
rule2.second = 0
|
||||||
schedule.scheduleJob(rule2, () => {
|
schedule.scheduleJob(rule2, () => {
|
||||||
console.log('Task: refresh ip info', new Date())
|
console.log('Task: refresh ip info', new Date())
|
||||||
getIpInfo()
|
getIpInfo()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ipSchedule()
|
ipSchedule()
|
||||||
|
|
||||||
module.exports = (httpServer) => {
|
module.exports = (httpServer) => {
|
||||||
const serverIo = new Server(httpServer, {
|
const serverIo = new Server(httpServer, {
|
||||||
path: '/client/os-info',
|
path: '/client/os-info',
|
||||||
cors: {
|
cors: {
|
||||||
origin: '*'
|
origin: '*'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
serverIo.on('connection', (socket) => {
|
serverIo.on('connection', (socket) => {
|
||||||
serverSockets[socket.id] = setInterval(async () => {
|
serverSockets[socket.id] = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
osData = await getOsData()
|
osData = await getOsData()
|
||||||
socket && socket.emit('client_data', Object.assign(osData, { ipInfo }))
|
socket && socket.emit('client_data', Object.assign(osData, { ipInfo }))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('客户端错误:', error)
|
console.error('客户端错误:', error)
|
||||||
socket && socket.emit('client_error', { error })
|
socket && socket.emit('client_error', { error })
|
||||||
}
|
}
|
||||||
}, 1500)
|
}, 1500)
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
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
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,83 +1,83 @@
|
|||||||
const osu = require('node-os-utils')
|
const osu = require('node-os-utils')
|
||||||
const os = require('os')
|
const os = require('os')
|
||||||
|
|
||||||
let cpu = osu.cpu
|
let cpu = osu.cpu
|
||||||
let mem = osu.mem
|
let mem = osu.mem
|
||||||
let drive = osu.drive
|
let drive = osu.drive
|
||||||
let netstat = osu.netstat
|
let netstat = osu.netstat
|
||||||
let osuOs = osu.os
|
let osuOs = osu.os
|
||||||
let users = osu.users
|
let users = osu.users
|
||||||
|
|
||||||
async function cpuInfo() {
|
async function cpuInfo() {
|
||||||
let cpuUsage = await cpu.usage(200)
|
let cpuUsage = await cpu.usage(200)
|
||||||
let cpuCount = cpu.count()
|
let cpuCount = cpu.count()
|
||||||
let cpuModel = cpu.model()
|
let cpuModel = cpu.model()
|
||||||
return {
|
return {
|
||||||
cpuUsage,
|
cpuUsage,
|
||||||
cpuCount,
|
cpuCount,
|
||||||
cpuModel
|
cpuModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function memInfo() {
|
async function memInfo() {
|
||||||
let memInfo = await mem.info()
|
let memInfo = await mem.info()
|
||||||
return {
|
return {
|
||||||
...memInfo
|
...memInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function driveInfo() {
|
async function driveInfo() {
|
||||||
let driveInfo = {}
|
let driveInfo = {}
|
||||||
try {
|
try {
|
||||||
driveInfo = await drive.info()
|
driveInfo = await drive.info()
|
||||||
} catch {
|
} catch {
|
||||||
// console.log(driveInfo)
|
// console.log(driveInfo)
|
||||||
}
|
}
|
||||||
return driveInfo
|
return driveInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
async function netstatInfo() {
|
async function netstatInfo() {
|
||||||
let netstatInfo = await netstat.inOut()
|
let netstatInfo = await netstat.inOut()
|
||||||
return netstatInfo === 'not supported' ? {} : netstatInfo
|
return netstatInfo === 'not supported' ? {} : netstatInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
async function osInfo() {
|
async function osInfo() {
|
||||||
let type = os.type()
|
let type = os.type()
|
||||||
let platform = os.platform()
|
let platform = os.platform()
|
||||||
let release = os.release()
|
let release = os.release()
|
||||||
let uptime = osuOs.uptime()
|
let uptime = osuOs.uptime()
|
||||||
let ip = osuOs.ip()
|
let ip = osuOs.ip()
|
||||||
let hostname = osuOs.hostname()
|
let hostname = osuOs.hostname()
|
||||||
let arch = osuOs.arch()
|
let arch = osuOs.arch()
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
platform,
|
platform,
|
||||||
release,
|
release,
|
||||||
ip,
|
ip,
|
||||||
hostname,
|
hostname,
|
||||||
arch,
|
arch,
|
||||||
uptime
|
uptime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openedCount() {
|
async function openedCount() {
|
||||||
let openedCount = await users.openedCount()
|
let openedCount = await users.openedCount()
|
||||||
return openedCount === 'not supported' ? 0 : openedCount
|
return openedCount === 'not supported' ? 0 : openedCount
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = async () => {
|
module.exports = async () => {
|
||||||
let data = {}
|
let data = {}
|
||||||
try {
|
try {
|
||||||
data = {
|
data = {
|
||||||
cpuInfo: await cpuInfo(),
|
cpuInfo: await cpuInfo(),
|
||||||
memInfo: await memInfo(),
|
memInfo: await memInfo(),
|
||||||
driveInfo: await driveInfo(),
|
driveInfo: await driveInfo(),
|
||||||
netstatInfo: await netstatInfo(),
|
netstatInfo: await netstatInfo(),
|
||||||
osInfo: await osInfo(),
|
osInfo: await osInfo(),
|
||||||
openedCount: await openedCount()
|
openedCount: await openedCount()
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
} catch(err){
|
} catch(err){
|
||||||
return err.toString()
|
return err.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
FROM node:16.15.0-alpine3.14
|
FROM node:16.15.0-alpine3.14
|
||||||
ARG TARGET_DIR=/easynode-server
|
ARG TARGET_DIR=/easynode-server
|
||||||
WORKDIR ${TARGET_DIR}
|
WORKDIR ${TARGET_DIR}
|
||||||
RUN yarn config set registry https://registry.npm.taobao.org
|
RUN yarn config set registry https://registry.npm.taobao.org
|
||||||
COPY package.json ${TARGET_DIR}
|
COPY package.json ${TARGET_DIR}
|
||||||
COPY yarn.lock ${TARGET_DIR}
|
COPY yarn.lock ${TARGET_DIR}
|
||||||
RUN yarn
|
RUN yarn
|
||||||
COPY . ${TARGET_DIR}
|
COPY . ${TARGET_DIR}
|
||||||
ENV HOST 0.0.0.0
|
ENV HOST 0.0.0.0
|
||||||
EXPOSE 8082
|
EXPOSE 8082
|
||||||
EXPOSE 8083
|
EXPOSE 8083
|
||||||
EXPOSE 22022
|
EXPOSE 22022
|
||||||
CMD ["npm", "run", "server"]
|
CMD ["npm", "run", "server"]
|
||||||
|
@ -1,30 +1,30 @@
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
|
|
||||||
const getCertificate =() => {
|
const getCertificate =() => {
|
||||||
try {
|
try {
|
||||||
return {
|
return {
|
||||||
cert: fs.readFileSync(path.join(__dirname, './pem/cert.pem')),
|
cert: fs.readFileSync(path.join(__dirname, './pem/cert.pem')),
|
||||||
key: fs.readFileSync(path.join(__dirname, './pem/key.pem'))
|
key: fs.readFileSync(path.join(__dirname, './pem/key.pem'))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = {
|
module.exports = {
|
||||||
domain: 'yourDomain', // 域名xxx.com
|
domain: '', // 域名(必须配置, 跨域使用[不配置将所有域名可访问api])
|
||||||
httpPort: 8082,
|
httpPort: 8082,
|
||||||
httpsPort: 8083,
|
httpsPort: 8083,
|
||||||
clientPort: 22022, // 勿更改
|
clientPort: 22022, // 勿更改
|
||||||
certificate: getCertificate(),
|
certificate: getCertificate(),
|
||||||
uploadDir: path.join(process.cwd(),'./app/static/upload'),
|
uploadDir: path.join(process.cwd(),'./app/static/upload'),
|
||||||
staticDir: path.join(process.cwd(),'./app/static'),
|
staticDir: path.join(process.cwd(),'./app/static'),
|
||||||
sshRecordPath: path.join(__dirname,'./storage/ssh-record.json'),
|
sshRecordPath: path.join(__dirname,'./storage/ssh-record.json'),
|
||||||
keyPath: path.join(__dirname,'./storage/key.json'),
|
keyPath: path.join(__dirname,'./storage/key.json'),
|
||||||
hostListPath: path.join(__dirname,'./storage/host-list.json'),
|
hostListPath: path.join(__dirname,'./storage/host-list.json'),
|
||||||
apiPrefix: '/api/v1',
|
apiPrefix: '/api/v1',
|
||||||
logConfig: {
|
logConfig: {
|
||||||
outDir: path.join(process.cwd(),'./app/logs'),
|
outDir: path.join(process.cwd(),'./app/logs'),
|
||||||
flag: false // 是否记录日志
|
flag: false // 是否记录日志
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"pwd": "admin",
|
"pwd": "admin",
|
||||||
"jwtExpires": "1h",
|
"jwtExpires": "1h",
|
||||||
"jwtSecret": "",
|
"commonKey": "",
|
||||||
"publicKey": "",
|
"publicKey": "",
|
||||||
"privateKey": ""
|
"privateKey": ""
|
||||||
}
|
}
|
@ -1,61 +1,68 @@
|
|||||||
const { readHostList, writeHostList } = require('../utils')
|
const { readHostList, writeHostList, readSSHRecord, writeSSHRecord } = require('../utils')
|
||||||
|
|
||||||
function getHostList({ res }) {
|
function getHostList({ res }) {
|
||||||
const data = readHostList()
|
const data = readHostList()
|
||||||
res.success({ data })
|
res.success({ data })
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveHost({ res, request }) {
|
function saveHost({ res, request }) {
|
||||||
let { body: { host: newHost, name } } = request
|
let { body: { host: newHost, name } } = request
|
||||||
if(!newHost || !name) return res.fail({ msg: '参数错误' })
|
if(!newHost || !name) return res.fail({ msg: '参数错误' })
|
||||||
let hostList = readHostList()
|
let hostList = readHostList()
|
||||||
if(hostList.some(({ host }) => host === newHost)) return res.fail({ msg: `主机${ newHost }已存在` })
|
if(hostList.some(({ host }) => host === newHost)) return res.fail({ msg: `主机${ newHost }已存在` })
|
||||||
hostList.push({ host: newHost, name })
|
hostList.push({ host: newHost, name })
|
||||||
writeHostList(hostList)
|
writeHostList(hostList)
|
||||||
res.success()
|
res.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateHost({ res, request }) {
|
function updateHost({ res, request }) {
|
||||||
let { body: { host: newHost, name: newName, oldHost } } = request
|
let { body: { host: newHost, name: newName, oldHost } } = request
|
||||||
if(!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' })
|
if(!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' })
|
||||||
let hostList = readHostList()
|
let hostList = readHostList()
|
||||||
if(!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `主机${ newHost }不存在` })
|
if(!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `主机${ newHost }不存在` })
|
||||||
let targetIdx = hostList.findIndex(({ host }) => host === oldHost)
|
let targetIdx = hostList.findIndex(({ host }) => host === oldHost)
|
||||||
hostList.splice(targetIdx, 1, { name: newName, host: newHost })
|
hostList.splice(targetIdx, 1, { name: newName, host: newHost })
|
||||||
writeHostList(hostList)
|
writeHostList(hostList)
|
||||||
res.success()
|
res.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeHost({ res, request }) {
|
function removeHost({ res, request }) {
|
||||||
let { body: { host } } = request
|
let { body: { host } } = request
|
||||||
let hostList = readHostList()
|
let hostList = readHostList()
|
||||||
let hostIdx = hostList.findIndex(item => item.host === host)
|
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)
|
hostList.splice(hostIdx, 1)
|
||||||
writeHostList(hostList)
|
writeHostList(hostList)
|
||||||
res.success({ data: `${ host }已移除` })
|
// 查询是否存在ssh记录
|
||||||
}
|
let sshRecord = readSSHRecord()
|
||||||
|
let sshIdx = sshRecord.findIndex(item => item.host === host)
|
||||||
function updateHostSort({ res, request }) {
|
let flag = sshIdx !== -1
|
||||||
let { body: { list } } = request
|
if(flag) sshRecord.splice(sshIdx, 1)
|
||||||
if(!list) return res.fail({ msg: '参数错误' })
|
writeSSHRecord(sshRecord)
|
||||||
let hostList = readHostList()
|
|
||||||
if(hostList.length !== list.length) return res.fail({ msg: '失败: host数量不匹配' })
|
res.success({ data: `${ host }已移除, ${ flag ? '并移除ssh记录' : '' }` })
|
||||||
let sortResult = []
|
}
|
||||||
for (let i = 0; i < list.length; i++) {
|
|
||||||
const curHost = list[i]
|
function updateHostSort({ res, request }) {
|
||||||
let temp = hostList.find(({ host }) => curHost.host === host)
|
let { body: { list } } = request
|
||||||
if(!temp) return res.fail({ msg: `查找失败: ${ curHost.name }` })
|
if(!list) return res.fail({ msg: '参数错误' })
|
||||||
sortResult.push(temp)
|
let hostList = readHostList()
|
||||||
}
|
if(hostList.length !== list.length) return res.fail({ msg: '失败: host数量不匹配' })
|
||||||
writeHostList(sortResult)
|
let sortResult = []
|
||||||
res.success({ msg: 'success' })
|
for (let i = 0; i < list.length; i++) {
|
||||||
}
|
const curHost = list[i]
|
||||||
|
let temp = hostList.find(({ host }) => curHost.host === host)
|
||||||
module.exports = {
|
if(!temp) return res.fail({ msg: `查找失败: ${ curHost.name }` })
|
||||||
getHostList,
|
sortResult.push(temp)
|
||||||
saveHost,
|
}
|
||||||
updateHost,
|
writeHostList(sortResult)
|
||||||
removeHost,
|
res.success({ msg: 'success' })
|
||||||
updateHostSort
|
}
|
||||||
}
|
|
||||||
|
module.exports = {
|
||||||
|
getHostList,
|
||||||
|
saveHost,
|
||||||
|
updateHost,
|
||||||
|
removeHost,
|
||||||
|
updateHostSort
|
||||||
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
let getOsData = require('../utils/os-data')
|
|
||||||
|
|
||||||
module.exports = async ({ res }) => {
|
|
||||||
let data = await getOsData()
|
|
||||||
res.success({ data })
|
|
||||||
}
|
|
@ -1,50 +1,57 @@
|
|||||||
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 }
|
||||||
let sshRecord = readSSHRecord()
|
if(!host || !port || !username || !type || !randomKey) return res.fail({ data: false, msg: '参数错误' })
|
||||||
let idx = sshRecord.findIndex(item => item.host === host)
|
// 再做一次对称加密(方便ssh连接时解密)
|
||||||
if(idx === -1)
|
record.randomKey = AESEncrypt(randomKey)
|
||||||
sshRecord.push(record)
|
let sshRecord = readSSHRecord()
|
||||||
else
|
let idx = sshRecord.findIndex(item => item.host === host)
|
||||||
sshRecord.splice(idx, 1, record)
|
if(idx === -1)
|
||||||
writeSSHRecord(sshRecord)
|
sshRecord.push(record)
|
||||||
res.success({ data: '保存成功' })
|
else
|
||||||
}
|
sshRecord.splice(idx, 1, record)
|
||||||
|
writeSSHRecord(sshRecord)
|
||||||
const removeSSH = async ({ res, request }) => {
|
console.log('新增凭证:', host)
|
||||||
let { body: { host } } = request
|
res.success({ data: '保存成功' })
|
||||||
let sshRecord = readSSHRecord()
|
}
|
||||||
let idx = sshRecord.findIndex(item => item.host === host)
|
|
||||||
if(idx === -1) return res.fail({ msg: '凭证不存在' })
|
const removeSSH = async ({ res, request }) => {
|
||||||
sshRecord.splice(idx, 1)
|
let { body: { host } } = request
|
||||||
writeSSHRecord(sshRecord)
|
let sshRecord = readSSHRecord()
|
||||||
res.success({ data: '移除成功' })
|
let idx = sshRecord.findIndex(item => item.host === host)
|
||||||
}
|
if(idx === -1) return res.fail({ msg: '凭证不存在' })
|
||||||
|
sshRecord.splice(idx, 1)
|
||||||
const existSSH = async ({ res, request }) => {
|
console.log('移除凭证:', host)
|
||||||
let { body: { host } } = request
|
writeSSHRecord(sshRecord)
|
||||||
let sshRecord = readSSHRecord()
|
res.success({ data: '移除成功' })
|
||||||
let idx = sshRecord.findIndex(item => item.host === host)
|
}
|
||||||
if(idx === -1) return res.success({ data: false })
|
|
||||||
res.success({ data: true })
|
const existSSH = async ({ res, request }) => {
|
||||||
}
|
let { body: { host } } = request
|
||||||
|
let sshRecord = readSSHRecord()
|
||||||
const getCommand = async ({ res, request }) => {
|
let idx = sshRecord.findIndex(item => item.host === host)
|
||||||
let { host } = request.query
|
console.log('查询凭证:', host)
|
||||||
if(!host) return res.fail({ data: false, msg: '参数错误' })
|
if(idx === -1) return res.success({ data: false }) // host不存在
|
||||||
let sshRecord = readSSHRecord()
|
res.success({ data: true }) // 存在
|
||||||
let record = sshRecord.find(item => item.host === host)
|
}
|
||||||
if(!record) return res.fail({ data: false, msg: 'host not found' })
|
|
||||||
const { command } = record
|
const getCommand = async ({ res, request }) => {
|
||||||
if(!command) return res.success({ data: false })
|
let { host } = request.query
|
||||||
res.success({ data: command })
|
if(!host) return res.fail({ data: false, msg: '参数错误' })
|
||||||
}
|
let sshRecord = readSSHRecord()
|
||||||
|
let record = sshRecord.find(item => item.host === host)
|
||||||
module.exports = {
|
console.log('查询登录后执行的指令:', host)
|
||||||
updateSSH,
|
if(!record) return res.fail({ data: false, msg: 'host not found' }) // host不存在
|
||||||
removeSSH,
|
const { command } = record
|
||||||
existSSH,
|
if(!command) return res.success({ data: false }) // command不存在
|
||||||
getCommand
|
res.success({ data: command }) // 存在
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
updateSSH,
|
||||||
|
removeSSH,
|
||||||
|
existSSH,
|
||||||
|
getCommand
|
||||||
|
}
|
||||||
|
@ -1,39 +1,66 @@
|
|||||||
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()
|
||||||
if(!data) return res.fail({ msg: 'publicKey not found, Try to restart the server', status: 500 })
|
if(!data) return res.fail({ msg: 'publicKey not found, Try to restart the server', status: 500 })
|
||||||
res.success({ data })
|
res.success({ data })
|
||||||
}
|
}
|
||||||
|
|
||||||
const login = async ({ res, request }) => {
|
const generateTokenAndRecordIP = async (clientIp) => {
|
||||||
let { body: { ciphertext } } = request
|
console.log('密码校验成功, 准备生成token')
|
||||||
if(!ciphertext) return res.fail({ msg: '参数错误' })
|
let { commonKey, jwtExpires } = readKey()
|
||||||
try {
|
let token = jwt.sign({ date: Date.now() }, commonKey, { expiresIn: jwtExpires }) // 生成token
|
||||||
const password = decrypt(ciphertext)
|
token = AESEncrypt(token) // 对称加密token后再传输给前端
|
||||||
let { pwd, jwtSecret, jwtExpires } = readKey()
|
console.log('aes对称加密token::', token)
|
||||||
if(password !== pwd) return res.fail({ msg: '密码错误' })
|
|
||||||
const token = jwt.sign({ date: Date.now() }, jwtSecret, { expiresIn: jwtExpires }) // 生成token
|
// 记录客户端登录IP用于判断是否异地(只保留最近10条)
|
||||||
res.success({ data: { token, jwtExpires } })
|
const localNetIPInfo = await getNetIPInfo(clientIp)
|
||||||
} catch (error) {
|
global.loginRecord.unshift(localNetIPInfo)
|
||||||
res.fail({ msg: '解密失败' })
|
if(global.loginRecord.length > 10) global.loginRecord = global.loginRecord.slice(0, 10)
|
||||||
}
|
return { token, jwtExpires }
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatePwd = async ({ res, request }) => {
|
const login = async ({ res, request }) => {
|
||||||
let { body: { oldPwd, newPwd } } = request
|
let { body: { ciphertext }, ip: clientIp } = request
|
||||||
oldPwd = decrypt(oldPwd)
|
if(!ciphertext) return res.fail({ msg: '参数错误' })
|
||||||
newPwd = decrypt(newPwd)
|
try {
|
||||||
let keyObj = readKey()
|
console.log('ciphertext', ciphertext)
|
||||||
if(oldPwd !== keyObj.pwd) return res.fail({ data: false, msg: '旧密码校验失败' })
|
let password = RSADecrypt(ciphertext)
|
||||||
keyObj.pwd = newPwd
|
let { pwd } = readKey()
|
||||||
writeKey(keyObj)
|
if(password === 'admin' && pwd === 'admin') {
|
||||||
res.success({ data: true, msg: 'success' })
|
const { token, jwtExpires } = await generateTokenAndRecordIP(clientIp)
|
||||||
}
|
return res.success({ data: { token, jwtExpires }, msg: '登录成功,请及时修改默认密码' })
|
||||||
|
}
|
||||||
module.exports = {
|
password = SHA1Encrypt(password)
|
||||||
login,
|
if(password !== pwd) return res.fail({ msg: '密码错误' })
|
||||||
getpublicKey,
|
const { token, jwtExpires } = await generateTokenAndRecordIP(clientIp)
|
||||||
updatePwd
|
return res.success({ data: { token, jwtExpires }, msg: '登录成功' })
|
||||||
}
|
} catch (error) {
|
||||||
|
console.log('解密失败:', error)
|
||||||
|
res.fail({ msg: '解密失败, 请查看服务端日志' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePwd = async ({ res, request }) => {
|
||||||
|
let { body: { oldPwd, newPwd } } = request
|
||||||
|
oldPwd = SHA1Encrypt(RSADecrypt(oldPwd))
|
||||||
|
newPwd = SHA1Encrypt(RSADecrypt(newPwd))
|
||||||
|
|
||||||
|
let keyObj = readKey()
|
||||||
|
if(oldPwd !== keyObj.pwd) return res.fail({ data: false, msg: '旧密码校验失败' })
|
||||||
|
keyObj.pwd = newPwd
|
||||||
|
writeKey(keyObj)
|
||||||
|
res.success({ data: true, msg: 'success' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLoginRecord = async ({ res }) => {
|
||||||
|
res.success({ data: global.loginRecord, msg: 'success' })
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
login,
|
||||||
|
getpublicKey,
|
||||||
|
updatePwd,
|
||||||
|
getLoginRecord
|
||||||
|
}
|
||||||
|
@ -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')
|
||||||
|
|
||||||
async function initIp() {
|
const isDev = !isProd()
|
||||||
if(!isProd()) return console.log('非生产环境不初始化保存本地IP')
|
|
||||||
const localNetIP = await getLocalNetIP()
|
// 存储本机IP, 供host列表接口调用
|
||||||
let vpsList = readHostList()
|
async function initIp() {
|
||||||
if(vpsList.some(({ host }) => host === localNetIP)) return console.log('本机IP已储存: ', localNetIP)
|
if(isDev) return console.log('非生产环境不初始化保存本地IP')
|
||||||
vpsList.unshift({ name: 'server-side-host', host: localNetIP })
|
const localNetIPInfo = await getNetIPInfo()
|
||||||
writeHostList(vpsList)
|
let vpsList = readHostList()
|
||||||
console.log('首次启动储存本机IP: ', localNetIP)
|
let { ip: localNetIP } = localNetIPInfo
|
||||||
}
|
if(vpsList.some(({ host }) => host === localNetIP)) return console.log('本机IP已储存: ', localNetIP)
|
||||||
|
vpsList.unshift({ name: 'server-side-host', host: localNetIP })
|
||||||
async function initRsa() {
|
writeHostList(vpsList)
|
||||||
let keyObj = readKey()
|
console.log('Task: 生产环境首次启动储存本机IP: ', localNetIP)
|
||||||
if(keyObj.privateKey && keyObj.publicKey) return console.log('公私钥已存在')
|
}
|
||||||
|
|
||||||
let key = new NodeRSA({ b: 1024 })
|
// 初始化公私钥, 供登录、保存ssh密钥/密码等加解密
|
||||||
key.setOptions({ encryptionScheme: 'pkcs1' })
|
async function initRsa() {
|
||||||
let privateKey = key.exportKey('pkcs1-private-pem')
|
let keyObj = readKey()
|
||||||
let publicKey = key.exportKey('pkcs8-public-pem')
|
if(keyObj.privateKey && keyObj.publicKey) return console.log('公私钥已存在[重新生成会导致已保存的ssh密钥信息失效]')
|
||||||
keyObj.privateKey = privateKey
|
|
||||||
keyObj.publicKey = publicKey
|
let key = new NodeRSA({ b: 1024 })
|
||||||
writeKey(keyObj)
|
key.setOptions({ encryptionScheme: 'pkcs1' })
|
||||||
console.log('新的公私钥已生成')
|
let privateKey = key.exportKey('pkcs1-private-pem')
|
||||||
}
|
let publicKey = key.exportKey('pkcs8-public-pem')
|
||||||
|
keyObj.privateKey = AESEncrypt(privateKey) // 加密私钥
|
||||||
function randomJWTSecret() {
|
keyObj.publicKey = publicKey // 公开公钥
|
||||||
let keyObj = readKey()
|
writeKey(keyObj)
|
||||||
if(keyObj.jwtSecret) return console.log('jwt secret已存在')
|
console.log('Task: 已生成新的非对称加密公私钥')
|
||||||
|
}
|
||||||
keyObj.jwtSecret = randomStr(32)
|
|
||||||
writeKey(keyObj)
|
// 随机的commonKey secret
|
||||||
console.log('已生成随机jwt secret')
|
function randomJWTSecret() {
|
||||||
}
|
let keyObj = readKey()
|
||||||
|
if(keyObj.commonKey) return console.log('commonKey密钥已存在')
|
||||||
module.exports = () => {
|
|
||||||
initIp()
|
keyObj.commonKey = randomStr(16)
|
||||||
initRsa()
|
writeKey(keyObj)
|
||||||
randomJWTSecret()
|
console.log('Task: 已生成新的随机commonKey密钥')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
randomJWTSecret() // 先生成全局唯一密钥
|
||||||
|
initIp()
|
||||||
|
initRsa()
|
||||||
|
// 用于记录客户端登录IP的列表
|
||||||
|
global.loginRecord = []
|
||||||
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
const { httpServer, httpsServer, clientHttpServer } = require('./server')
|
const { httpServer, httpsServer, clientHttpServer } = require('./server')
|
||||||
const initLocal = require('./init')
|
const initLocal = require('./init')
|
||||||
|
|
||||||
initLocal()
|
initLocal()
|
||||||
|
|
||||||
httpServer()
|
httpServer()
|
||||||
|
|
||||||
httpsServer()
|
httpsServer()
|
||||||
|
|
||||||
clientHttpServer()
|
clientHttpServer()
|
||||||
|
@ -1,24 +1,28 @@
|
|||||||
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 { path, headers: { token } } = request
|
const useAuth = async ({ request, res }, next) => {
|
||||||
if(whitePath.includes(path)) return next()
|
const { path, headers: { token } } = request
|
||||||
if(!token) return res.fail({ msg: '未登录', status: 403 })
|
console.log('path: ', path)
|
||||||
const { code, msg } = verifyToken(token)
|
// console.log('token: ', token)
|
||||||
switch(code) {
|
if(whitePath.includes(path)) return next()
|
||||||
case 1:
|
if(!token) return res.fail({ msg: '未登录', status: 403 })
|
||||||
return await next()
|
// 验证token
|
||||||
case -1:
|
const { code, msg } = verifyAuth(token, request.ip)
|
||||||
return res.fail({ msg, status: 401 })
|
switch(code) {
|
||||||
case -2:
|
case 1:
|
||||||
return res.fail({ msg: '登录态错误, 请重新登录', status: 401, data: msg })
|
return await next()
|
||||||
}
|
case -1:
|
||||||
}
|
return res.fail({ msg, status: 401 })
|
||||||
|
case -2:
|
||||||
module.exports = useJwt
|
return res.fail({ msg: '登录态错误, 请重新登录', status: 401, data: msg })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = useAuth
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
@ -1,5 +1,5 @@
|
|||||||
const compress = require('koa-compress')
|
const compress = require('koa-compress')
|
||||||
|
|
||||||
const options = { threshold: 2048 }
|
const options = { threshold: 2048 }
|
||||||
|
|
||||||
module.exports = compress(options)
|
module.exports = compress(options)
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
const cors = require('@koa/cors')
|
const cors = require('@koa/cors')
|
||||||
// const { domain } = require('../config')
|
const { domain } = require('../config')
|
||||||
|
|
||||||
const useCors = cors({
|
// 跨域处理
|
||||||
origin: ({ req }) => {
|
const useCors = cors({
|
||||||
// console.log(req.headers.origin)
|
origin: ({ req }) => {
|
||||||
// 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' ]
|
})
|
||||||
})
|
|
||||||
|
module.exports = useCors
|
||||||
module.exports = useCors
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
const { historyApiFallback } = require('koa2-connect-history-api-fallback')
|
const { historyApiFallback } = require('koa2-connect-history-api-fallback')
|
||||||
|
|
||||||
module.exports = historyApiFallback({ whiteList: ['/api'] })
|
module.exports = historyApiFallback({ whiteList: ['/api'] })
|
||||||
|
@ -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 = [
|
// 注意注册顺序
|
||||||
compress,
|
module.exports = [
|
||||||
history,
|
compress,
|
||||||
useStatic,
|
history,
|
||||||
useCors,
|
useStatic, // staic先注册,不然会被jwt拦截
|
||||||
responseHandler,
|
useCors,
|
||||||
useKoaBody,
|
responseHandler,
|
||||||
useLog,
|
useKoaBody, // 先处理body,log和router都要用到
|
||||||
useJwt,
|
useLog, // 日志记录开始【该module使用到了fs.mkdir()等读写api, 设置保存日志的目录需使用process.cwd()】
|
||||||
useAllowedMethods,
|
useAuth,
|
||||||
useRoutes
|
useAllowedMethods,
|
||||||
]
|
useRoutes
|
||||||
|
]
|
||||||
|
@ -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
|
||||||
|
@ -1,31 +1,33 @@
|
|||||||
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.status = status || 400
|
ctx.res.fail = ({ status, msg = 'fail', data = {} } = {}) => {
|
||||||
ctx.body = {
|
ctx.status = status || 400 // 响应失败默认 400
|
||||||
status,
|
ctx.body = {
|
||||||
data,
|
status, // 失败默认 400
|
||||||
msg
|
data,
|
||||||
}
|
msg
|
||||||
}
|
}
|
||||||
|
}
|
||||||
try {
|
|
||||||
await next()
|
// 错误响应捕获
|
||||||
} catch (err) {
|
try {
|
||||||
console.log('中间件错误:', err)
|
await next() // 每个中间件都需等待next完成调用,不然会返回404给前端!!!
|
||||||
if (err.status)
|
} catch (err) {
|
||||||
ctx.res.fail({ status: err.status, msg: err.message })
|
console.log('中间件错误:', err)
|
||||||
else
|
if (err.status)
|
||||||
ctx.app.emit('error', err, ctx)
|
ctx.res.fail({ status: err.status, msg: err.message }) // 自己主动抛出的错误 throwError
|
||||||
}
|
else
|
||||||
}
|
ctx.app.emit('error', err, ctx) // 程序运行时的错误 main.js中监听
|
||||||
|
}
|
||||||
module.exports = responseHandler
|
}
|
||||||
|
|
||||||
|
module.exports = responseHandler
|
||||||
|
@ -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 = {
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
const { apiPrefix } = require('../config')
|
const { apiPrefix } = require('../config')
|
||||||
const koaRouter = require('koa-router')
|
const koaRouter = require('koa-router')
|
||||||
const router = new koaRouter({ prefix: apiPrefix })
|
const router = new koaRouter({ prefix: apiPrefix })
|
||||||
|
|
||||||
const routeList = require('./routes')
|
const routeList = require('./routes')
|
||||||
|
|
||||||
routeList.forEach(item => {
|
// 统一注册路由
|
||||||
const { method, path, controller } = item
|
routeList.forEach(item => {
|
||||||
router[method](path, controller)
|
const { method, path, controller } = item
|
||||||
})
|
router[method](path, controller)
|
||||||
|
})
|
||||||
module.exports = router
|
|
||||||
|
module.exports = router
|
||||||
|
@ -1,74 +1,74 @@
|
|||||||
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, getLoginRecord } = require('../controller/user')
|
||||||
const { login, getpublicKey, updatePwd } = require('../controller/user')
|
|
||||||
|
// 路由统一管理
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
method: 'get',
|
method: 'post',
|
||||||
path: '/os-info',
|
path: '/update-ssh',
|
||||||
controller: osInfo
|
controller: updateSSH
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: '/update-ssh',
|
path: '/remove-ssh',
|
||||||
controller: updateSSH
|
controller: removeSSH
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: '/remove-ssh',
|
path: '/exist-ssh',
|
||||||
controller: removeSSH
|
controller: existSSH
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'post',
|
method: 'get',
|
||||||
path: '/exist-ssh',
|
path: '/command',
|
||||||
controller: existSSH
|
controller: getCommand
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/command',
|
path: '/host-list',
|
||||||
controller: getCommand
|
controller: getHostList
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'get',
|
method: 'post',
|
||||||
path: '/host-list',
|
path: '/host-save',
|
||||||
controller: getHostList
|
controller: saveHost
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'post',
|
method: 'put',
|
||||||
path: '/host-save',
|
path: '/host-save',
|
||||||
controller: saveHost
|
controller: updateHost
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'put',
|
method: 'post',
|
||||||
path: '/host-save',
|
path: '/host-remove',
|
||||||
controller: updateHost
|
controller: removeHost
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'post',
|
method: 'put',
|
||||||
path: '/host-remove',
|
path: '/host-sort',
|
||||||
controller: removeHost
|
controller: updateHostSort
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'put',
|
method: 'get',
|
||||||
path: '/host-sort',
|
path: '/get-pub-pem',
|
||||||
controller: updateHostSort
|
controller: getpublicKey
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'get',
|
method: 'post',
|
||||||
path: '/get-pub-pem',
|
path: '/login',
|
||||||
controller: getpublicKey
|
controller: login
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'post',
|
method: 'put',
|
||||||
path: '/login',
|
path: '/pwd',
|
||||||
controller: login
|
controller: updatePwd
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'put',
|
method: 'get',
|
||||||
path: '/pwd',
|
path: '/get-login-record',
|
||||||
controller: updatePwd
|
controller: getLoginRecord
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
module.exports = routes
|
module.exports = routes
|
||||||
|
@ -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 = {
|
||||||
|
@ -1,73 +1,96 @@
|
|||||||
const { Server: ServerIO } = require('socket.io')
|
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()
|
||||||
hostList
|
hostList
|
||||||
.map(({ host }) => {
|
.map(({ host }) => {
|
||||||
let clientSocket = ClientIO(`http://${ host }:${ clientPort }`, {
|
let clientSocket = ClientIO(`http://${ host }:${ clientPort }`, {
|
||||||
path: '/client/os-info',
|
path: '/client/os-info',
|
||||||
forceNew: true,
|
forceNew: true,
|
||||||
reconnectionDelay: 3000,
|
timeout: 5000,
|
||||||
reconnectionAttempts: 1
|
reconnectionDelay: 3000,
|
||||||
})
|
reconnectionAttempts: 100
|
||||||
clientSockets[socketId].push(clientSocket)
|
})
|
||||||
return {
|
// 将与客户端连接的socket实例保存起来,web端断开时关闭这些连接
|
||||||
host,
|
clientSockets[socketId].push(clientSocket)
|
||||||
clientSocket
|
return {
|
||||||
}
|
host,
|
||||||
})
|
clientSocket
|
||||||
.map(({ host, clientSocket }) => {
|
}
|
||||||
clientSocket
|
})
|
||||||
.on('connect', () => {
|
.map(({ host, clientSocket }) => {
|
||||||
clientSocket.on('client_data', (osData) => {
|
clientSocket
|
||||||
clientsData[host] = osData
|
.on('connect', () => {
|
||||||
})
|
console.log('client connect success:', host)
|
||||||
clientSocket.on('client_error', (error) => {
|
clientSocket.on('client_data', (osData) => {
|
||||||
clientsData[host] = error
|
clientsData[host] = osData
|
||||||
})
|
})
|
||||||
})
|
clientSocket.on('client_error', (error) => {
|
||||||
.on('connect_error', () => {
|
clientsData[host] = error
|
||||||
clientsData[host] = null
|
})
|
||||||
})
|
})
|
||||||
.on('disconnect', () => {
|
.on('connect_error', (error) => {
|
||||||
clientsData[host] = null
|
console.log('client connect fail:', host, error.message)
|
||||||
})
|
clientsData[host] = null
|
||||||
})
|
})
|
||||||
}
|
.on('disconnect', () => {
|
||||||
|
console.log('client connect disconnect:', host)
|
||||||
module.exports = (httpServer) => {
|
clientsData[host] = null
|
||||||
const serverIo = new ServerIO(httpServer, {
|
})
|
||||||
path: '/clients',
|
})
|
||||||
cors: {
|
}
|
||||||
}
|
|
||||||
})
|
module.exports = (httpServer) => {
|
||||||
|
const serverIo = new ServerIO(httpServer, {
|
||||||
serverIo.on('connection', (socket) => {
|
path: '/clients',
|
||||||
socket.on('init_clients_data', ({ token }) => {
|
cors: {
|
||||||
const { code } = verifyToken(token)
|
origin: '*' // 需配置跨域
|
||||||
if(code !== 1) return socket.emit('token_verify_fail', 'token无效')
|
}
|
||||||
|
})
|
||||||
clientSockets[socket.id] = []
|
|
||||||
|
serverIo.on('connection', (socket) => {
|
||||||
getClientsInfo(socket.id)
|
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
||||||
|
let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
|
||||||
socket.emit('clients_data', clientsData)
|
socket.on('init_clients_data', ({ token }) => {
|
||||||
|
// 校验登录态
|
||||||
timer = setInterval(() => {
|
const { code, msg } = verifyAuth(token, clientIp)
|
||||||
socket.emit('clients_data', clientsData)
|
if(code !== 1) {
|
||||||
}, 1500)
|
socket.emit('token_verify_fail', msg || '鉴权失败')
|
||||||
|
socket.disconnect()
|
||||||
socket.on('disconnect', () => {
|
return
|
||||||
if(timer) clearInterval(timer)
|
}
|
||||||
clientSockets[socket.id].forEach(socket => socket.close && socket.close())
|
|
||||||
delete clientSockets[socket.id]
|
// 收集web端连接的id
|
||||||
})
|
clientSockets[socket.id] = []
|
||||||
})
|
console.log('client连接socketId: ', socket.id, 'clients-socket已连接数: ', Object.keys(clientSockets).length)
|
||||||
})
|
|
||||||
}
|
// 获取客户端数据
|
||||||
|
getClientsInfo(socket.id)
|
||||||
|
|
||||||
|
// 立即推送一次
|
||||||
|
socket.emit('clients_data', clientsData)
|
||||||
|
|
||||||
|
// 向web端推送数据
|
||||||
|
let timer = null
|
||||||
|
timer = setInterval(() => {
|
||||||
|
socket.emit('clients_data', clientsData)
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
// 关闭连接
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
// 防止内存泄漏
|
||||||
|
if(timer) clearInterval(timer)
|
||||||
|
// 当web端与服务端断开连接时, 服务端与每个客户端的socket也应该断开连接
|
||||||
|
clientSockets[socket.id].forEach(socket => socket.close && socket.close())
|
||||||
|
delete clientSockets[socket.id]
|
||||||
|
console.log('断开socketId: ', socket.id, 'clients-socket剩余连接数: ', Object.keys(clientSockets).length)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
74
server/app/socket/host-status.js
Normal file
74
server/app/socket/host-status.js
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,71 +1,92 @@
|
|||||||
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', () => {
|
||||||
vps.end()
|
console.log('关闭终端')
|
||||||
})
|
vps.end()
|
||||||
socket.on('input', key => {
|
})
|
||||||
if(vps._sock.writable === false) return console.log('终端连接已关闭')
|
socket.on('input', key => {
|
||||||
stream.write(key)
|
if(vps._sock.writable === false) return console.log('终端连接已关闭')
|
||||||
})
|
stream.write(key)
|
||||||
socket.emit('connect_terminal')
|
})
|
||||||
|
socket.emit('connect_terminal')
|
||||||
})
|
|
||||||
}
|
socket.on('resize', ({ rows, cols }) => {
|
||||||
|
stream.setWindow(rows, cols)
|
||||||
module.exports = (httpServer) => {
|
})
|
||||||
const serverIo = new Server(httpServer, {
|
})
|
||||||
path: '/terminal',
|
}
|
||||||
cors: {
|
|
||||||
origin: '*'
|
module.exports = (httpServer) => {
|
||||||
}
|
const serverIo = new Server(httpServer, {
|
||||||
})
|
path: '/terminal',
|
||||||
serverIo.on('connection', (socket) => {
|
cors: {
|
||||||
let vps = new Client()
|
origin: '*'
|
||||||
|
}
|
||||||
socket.on('create', ({ host: ip, token }) => {
|
})
|
||||||
|
serverIo.on('connection', (socket) => {
|
||||||
const { code } = verifyToken(token)
|
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
||||||
if(code !== 1) return socket.emit('token_verify_fail')
|
let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
|
||||||
|
let vps = new Client()
|
||||||
const sshRecord = readSSHRecord()
|
console.log('terminal websocket 已连接')
|
||||||
let loginInfo = sshRecord.find(item => item.host === ip)
|
|
||||||
if(!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ ip }】凭证`)
|
socket.on('create', ({ host: ip, token }) => {
|
||||||
const { type, host, port, username } = loginInfo
|
const { code } = verifyAuth(token, clientIp)
|
||||||
try {
|
if(code !== 1) {
|
||||||
vps
|
socket.emit('token_verify_fail')
|
||||||
.on('ready', () => {
|
socket.disconnect()
|
||||||
socket.emit('connect_success', `已连接到服务器:${ host }`)
|
return
|
||||||
createTerminal(socket, vps)
|
}
|
||||||
})
|
// console.log('code:', code)
|
||||||
.on('error', (err) => {
|
const sshRecord = readSSHRecord()
|
||||||
socket.emit('connect_fail', err.message)
|
let loginInfo = sshRecord.find(item => item.host === ip)
|
||||||
})
|
if(!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ ip }】凭证`)
|
||||||
.connect({
|
let { type, host, port, username, randomKey } = loginInfo
|
||||||
type: 'privateKey',
|
try {
|
||||||
host,
|
// 解密放到try里面,防止报错【公私钥必须配对, 否则需要重新添加服务器密钥】
|
||||||
port,
|
randomKey = AESDecrypt(randomKey) // 先对称解密key
|
||||||
username,
|
randomKey = RSADecrypt(randomKey) // 再非对称解密key
|
||||||
[type]: loginInfo[type]
|
loginInfo[type] = AESDecrypt(loginInfo[type], randomKey) // 对称解密ssh密钥
|
||||||
|
console.log('准备连接服务器:', host)
|
||||||
})
|
vps
|
||||||
} catch (err) {
|
.on('ready', () => {
|
||||||
socket.emit('create_fail', err.message)
|
console.log('已连接到服务器:', host)
|
||||||
}
|
socket.emit('connect_success', `已连接到服务器:${ host }`)
|
||||||
})
|
createTerminal(socket, vps)
|
||||||
|
})
|
||||||
socket.on('disconnect', () => {
|
.on('error', (err) => {
|
||||||
vps.end()
|
console.log('连接失败:', err.level)
|
||||||
vps.destroy()
|
socket.emit('connect_fail', err.message)
|
||||||
vps = null
|
})
|
||||||
})
|
.connect({
|
||||||
})
|
type: 'privateKey',
|
||||||
}
|
host,
|
||||||
|
port,
|
||||||
|
username,
|
||||||
|
[type]: loginInfo[type]
|
||||||
|
// debug: (info) => {
|
||||||
|
// console.log(info)
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.log('创建失败:', err.message)
|
||||||
|
socket.emit('create_fail', err.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('disconnect', (reason) => {
|
||||||
|
console.log('终端连接断开:', reason)
|
||||||
|
vps.end()
|
||||||
|
vps.destroy()
|
||||||
|
vps = null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
87
server/app/static/assets/index.704cb447.js
Normal file
87
server/app/static/assets/index.704cb447.js
Normal file
File diff suppressed because one or more lines are too long
32
server/app/static/assets/index.fdac59aa.css
Normal file
32
server/app/static/assets/index.fdac59aa.css
Normal file
File diff suppressed because one or more lines are too long
@ -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>
|
||||||
|
@ -1,120 +1,189 @@
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const jwt = require('jsonwebtoken')
|
const CryptoJS = require('crypto-js')
|
||||||
const axios = require('axios')
|
const rawCrypto = require('crypto')
|
||||||
const NodeRSA = require('node-rsa')
|
const NodeRSA = require('node-rsa')
|
||||||
|
const jwt = require('jsonwebtoken')
|
||||||
const { sshRecordPath, hostListPath, keyPath } = require('../config')
|
const axios = require('axios')
|
||||||
|
const request = axios.create({ timeout: 3000 })
|
||||||
const readSSHRecord = () => {
|
|
||||||
let list
|
const { sshRecordPath, hostListPath, keyPath } = require('../config')
|
||||||
try {
|
|
||||||
list = JSON.parse(fs.readFileSync(sshRecordPath, 'utf8'))
|
const readSSHRecord = () => {
|
||||||
} catch (error) {
|
let list
|
||||||
writeSSHRecord([])
|
try {
|
||||||
}
|
list = JSON.parse(fs.readFileSync(sshRecordPath, 'utf8'))
|
||||||
return list || []
|
} catch (error) {
|
||||||
}
|
console.log('读取ssh-record错误, 即将重置ssh列表: ', error)
|
||||||
|
writeSSHRecord([])
|
||||||
const writeSSHRecord = (record = []) => {
|
}
|
||||||
fs.writeFileSync(sshRecordPath, JSON.stringify(record, null, 2))
|
return list || []
|
||||||
}
|
}
|
||||||
|
|
||||||
const readHostList = () => {
|
const writeSSHRecord = (record = []) => {
|
||||||
let list
|
fs.writeFileSync(sshRecordPath, JSON.stringify(record, null, 2))
|
||||||
try {
|
}
|
||||||
list = JSON.parse(fs.readFileSync(hostListPath, 'utf8'))
|
|
||||||
} catch (error) {
|
const readHostList = () => {
|
||||||
writeHostList([])
|
let list
|
||||||
}
|
try {
|
||||||
return list || []
|
list = JSON.parse(fs.readFileSync(hostListPath, 'utf8'))
|
||||||
}
|
} catch (error) {
|
||||||
|
console.log('读取host-list错误, 即将重置host列表: ', error)
|
||||||
const writeHostList = (record = []) => {
|
writeHostList([])
|
||||||
fs.writeFileSync(hostListPath, JSON.stringify(record, null, 2))
|
}
|
||||||
}
|
return list || []
|
||||||
|
}
|
||||||
const readKey = () => {
|
|
||||||
let keyObj = JSON.parse(fs.readFileSync(keyPath, 'utf8'))
|
const writeHostList = (record = []) => {
|
||||||
return keyObj
|
fs.writeFileSync(hostListPath, JSON.stringify(record, null, 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
const writeKey = (keyObj = {}) => {
|
const readKey = () => {
|
||||||
fs.writeFileSync(keyPath, JSON.stringify(keyObj, null, 2))
|
let keyObj = JSON.parse(fs.readFileSync(keyPath, 'utf8'))
|
||||||
}
|
return keyObj
|
||||||
|
}
|
||||||
const getLocalNetIP = async () => {
|
|
||||||
try {
|
const writeKey = (keyObj = {}) => {
|
||||||
let ipUrls = ['http://ip-api.com/json/?lang=zh-CN', 'http://whois.pconline.com.cn/ipJson.jsp?json=true']
|
fs.writeFileSync(keyPath, JSON.stringify(keyObj, null, 2))
|
||||||
let { data } = await Promise.race(ipUrls.map(url => axios.get(url)))
|
}
|
||||||
return data.ip || data.query
|
|
||||||
} catch (error) {
|
// 为空时请求本地IP
|
||||||
console.error('getIpInfo Error: ', error)
|
const getNetIPInfo = async (ip = '') => {
|
||||||
return {
|
try {
|
||||||
ip: '未知',
|
let date = getUTCDate(8)
|
||||||
country: '未知',
|
let ipUrls = [`http://ip-api.com/json/${ ip }?lang=zh-CN`, `http://whois.pconline.com.cn/ipJson.jsp?json=true&ip=${ ip }`]
|
||||||
city: '未知',
|
let result = await Promise.allSettled(ipUrls.map(url => request.get(url)))
|
||||||
error
|
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 }
|
||||||
const throwError = ({ status = 500, msg = 'defalut error' } = {}) => {
|
}
|
||||||
const err = new Error(msg)
|
if(pconline.status === 'fulfilled') {
|
||||||
err.status = status
|
let { ip, pro, city, addr } = pconline.value.data
|
||||||
throw err
|
// console.log({ ip, country: pro || addr, city })
|
||||||
}
|
return { ip, country: pro || addr, city, date }
|
||||||
|
}
|
||||||
const isIP = (ip = '') => {
|
throw Error('获取IP信息API出错,请排查或更新API')
|
||||||
const isIPv4 = /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/
|
} catch (error) {
|
||||||
const isIPv6 = /^([\da-fA-F]{1,4}:){7}[\da-fA-F]{1,4}|:((:[\da−fA−F]1,4)1,6|:)|:((:[\da−fA−F]1,4)1,6|:)|^[\da-fA-F]{1,4}:((:[\da-fA-F]{1,4}){1,5}|:)|([\da−fA−F]1,4:)2((:[\da−fA−F]1,4)1,4|:)|([\da−fA−F]1,4:)2((:[\da−fA−F]1,4)1,4|:)|^([\da-fA-F]{1,4}:){3}((:[\da-fA-F]{1,4}){1,3}|:)|([\da−fA−F]1,4:)4((:[\da−fA−F]1,4)1,2|:)|([\da−fA−F]1,4:)4((:[\da−fA−F]1,4)1,2|:)|^([\da-fA-F]{1,4}:){5}:([\da-fA-F]{1,4})?|([\da−fA−F]1,4:)6:|([\da−fA−F]1,4:)6:/
|
console.error('getIpInfo Error: ', error)
|
||||||
return isIPv4.test(ip) || isIPv6.test(ip)
|
return {
|
||||||
}
|
ip: '未知',
|
||||||
|
country: '未知',
|
||||||
const randomStr = (e) =>{
|
city: '未知',
|
||||||
e = e || 32
|
error
|
||||||
let str = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678',
|
}
|
||||||
a = str.length,
|
}
|
||||||
res = ''
|
}
|
||||||
for (let i = 0; i < e; i++) res += str.charAt(Math.floor(Math.random() * a))
|
|
||||||
return res
|
const throwError = ({ status = 500, msg = 'defalut error' } = {}) => {
|
||||||
}
|
const err = new Error(msg)
|
||||||
|
err.status = status // 主动抛错
|
||||||
const verifyToken = (token) =>{
|
throw err
|
||||||
const { jwtSecret } = readKey()
|
}
|
||||||
try {
|
|
||||||
const { exp } = jwt.verify(token, jwtSecret)
|
const isIP = (ip = '') => {
|
||||||
if(Date.now() > (exp * 1000)) return { code: -1, msg: 'token expires' }
|
const isIPv4 = /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/
|
||||||
return { code: 1, msg: 'success' }
|
const isIPv6 = /^([\da-fA-F]{1,4}:){7}[\da-fA-F]{1,4}|:((:[\da−fA−F]1,4)1,6|:)|:((:[\da−fA−F]1,4)1,6|:)|^[\da-fA-F]{1,4}:((:[\da-fA-F]{1,4}){1,5}|:)|([\da−fA−F]1,4:)2((:[\da−fA−F]1,4)1,4|:)|([\da−fA−F]1,4:)2((:[\da−fA−F]1,4)1,4|:)|^([\da-fA-F]{1,4}:){3}((:[\da-fA-F]{1,4}){1,3}|:)|([\da−fA−F]1,4:)4((:[\da−fA−F]1,4)1,2|:)|([\da−fA−F]1,4:)4((:[\da−fA−F]1,4)1,2|:)|^([\da-fA-F]{1,4}:){5}:([\da-fA-F]{1,4})?|([\da−fA−F]1,4:)6:|([\da−fA−F]1,4:)6:/
|
||||||
} catch (error) {
|
return isIPv4.test(ip) || isIPv6.test(ip)
|
||||||
return { code: -2, msg: error }
|
}
|
||||||
}
|
|
||||||
}
|
const randomStr = (e) =>{
|
||||||
|
e = e || 16
|
||||||
const isProd = () => {
|
let str = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678',
|
||||||
const EXEC_ENV = process.env.EXEC_ENV || 'production'
|
a = str.length,
|
||||||
return EXEC_ENV === 'production'
|
res = ''
|
||||||
}
|
for (let i = 0; i < e; i++) res += str.charAt(Math.floor(Math.random() * a))
|
||||||
|
return res
|
||||||
const decrypt = (ciphertext) => {
|
}
|
||||||
let { privateKey } = readKey()
|
|
||||||
const rsakey = new NodeRSA(privateKey)
|
// 校验token与登录IP
|
||||||
rsakey.setOptions({ encryptionScheme: 'pkcs1' })
|
const verifyAuth = (token, clientIp) =>{
|
||||||
const plaintext = rsakey.decrypt(ciphertext, 'utf8')
|
token = AESDecrypt(token) // 先aes解密
|
||||||
return plaintext
|
const { commonKey } = readKey()
|
||||||
}
|
try {
|
||||||
|
const { exp } = jwt.verify(token, commonKey)
|
||||||
module.exports = {
|
// console.log('校验token:', new Date(), '---', new Date(exp * 1000))
|
||||||
readSSHRecord,
|
if(Date.now() > (exp * 1000)) return { code: -1, msg: 'token expires' } // 过期
|
||||||
writeSSHRecord,
|
|
||||||
readHostList,
|
let lastLoginIp = global.loginRecord[0] ? global.loginRecord[0].ip : ''
|
||||||
writeHostList,
|
console.log('校验客户端IP:', clientIp)
|
||||||
getLocalNetIP,
|
console.log('最后登录的IP:', lastLoginIp)
|
||||||
throwError,
|
// 判断: (生产环境)clientIp与上次登录成功IP不一致
|
||||||
isIP,
|
if(isProd() && (!lastLoginIp || !clientIp || !clientIp.includes(lastLoginIp))) {
|
||||||
readKey,
|
return { code: -1, msg: '登录IP发生变化, 需重新登录' } // IP与上次登录访问的不一致
|
||||||
writeKey,
|
}
|
||||||
randomStr,
|
// console.log('token验证成功')
|
||||||
verifyToken,
|
return { code: 1, msg: 'success' } // 验证成功
|
||||||
isProd,
|
} catch (error) {
|
||||||
decrypt
|
// console.log('token校验错误:', error)
|
||||||
|
return { code: -2, msg: error } // token错误, 验证失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isProd = () => {
|
||||||
|
const EXEC_ENV = process.env.EXEC_ENV || 'production'
|
||||||
|
return EXEC_ENV === 'production'
|
||||||
|
}
|
||||||
|
|
||||||
|
// rsa非对称 私钥解密
|
||||||
|
const RSADecrypt = (ciphertext) => {
|
||||||
|
if(!ciphertext) return
|
||||||
|
let { privateKey } = readKey()
|
||||||
|
privateKey = AESDecrypt(privateKey) // 先解密私钥
|
||||||
|
const rsakey = new NodeRSA(privateKey)
|
||||||
|
rsakey.setOptions({ encryptionScheme: 'pkcs1' }) // Must Set It When Frontend Use jsencrypt
|
||||||
|
const plaintext = rsakey.decrypt(ciphertext, 'utf8')
|
||||||
|
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 = {
|
||||||
|
readSSHRecord,
|
||||||
|
writeSSHRecord,
|
||||||
|
readHostList,
|
||||||
|
writeHostList,
|
||||||
|
getNetIPInfo,
|
||||||
|
throwError,
|
||||||
|
isIP,
|
||||||
|
readKey,
|
||||||
|
writeKey,
|
||||||
|
randomStr,
|
||||||
|
verifyAuth,
|
||||||
|
isProd,
|
||||||
|
RSADecrypt,
|
||||||
|
AESEncrypt,
|
||||||
|
AESDecrypt,
|
||||||
|
SHA1Encrypt,
|
||||||
|
getUTCDate
|
||||||
}
|
}
|
@ -1,84 +1,84 @@
|
|||||||
const osu = require('node-os-utils')
|
const osu = require('node-os-utils')
|
||||||
const os = require('os')
|
const os = require('os')
|
||||||
|
|
||||||
let cpu = osu.cpu
|
let cpu = osu.cpu
|
||||||
let mem = osu.mem
|
let mem = osu.mem
|
||||||
let drive = osu.drive
|
let drive = osu.drive
|
||||||
let netstat = osu.netstat
|
let netstat = osu.netstat
|
||||||
let osuOs = osu.os
|
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 {
|
||||||
cpuUsage,
|
cpuUsage,
|
||||||
cpuCount,
|
cpuCount,
|
||||||
cpuModel
|
cpuModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function memInfo() {
|
async function memInfo() {
|
||||||
let memInfo = await mem.info()
|
let memInfo = await mem.info()
|
||||||
return {
|
return {
|
||||||
...memInfo
|
...memInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function driveInfo() {
|
async function driveInfo() {
|
||||||
let driveInfo = {}
|
let driveInfo = {}
|
||||||
try {
|
try {
|
||||||
driveInfo = await drive.info()
|
driveInfo = await drive.info()
|
||||||
} catch {
|
} catch {
|
||||||
// console.log(driveInfo)
|
// console.log(driveInfo)
|
||||||
}
|
}
|
||||||
return driveInfo
|
return 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
|
||||||
}
|
}
|
||||||
|
|
||||||
async function osInfo() {
|
async function osInfo() {
|
||||||
let type = os.type()
|
let type = os.type()
|
||||||
let platform = os.platform()
|
let platform = os.platform()
|
||||||
let release = os.release()
|
let release = os.release()
|
||||||
let uptime = osuOs.uptime()
|
let uptime = osuOs.uptime()
|
||||||
let ip = osuOs.ip()
|
let ip = osuOs.ip()
|
||||||
let hostname = osuOs.hostname()
|
let hostname = osuOs.hostname()
|
||||||
let arch = osuOs.arch()
|
let arch = osuOs.arch()
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
platform,
|
platform,
|
||||||
release,
|
release,
|
||||||
ip,
|
ip,
|
||||||
hostname,
|
hostname,
|
||||||
arch,
|
arch,
|
||||||
uptime
|
uptime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openedCount() {
|
async function openedCount() {
|
||||||
let openedCount = await users.openedCount()
|
let openedCount = await users.openedCount()
|
||||||
return openedCount === 'not supported' ? 0 : openedCount
|
return openedCount === 'not supported' ? 0 : openedCount
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = async () => {
|
module.exports = async () => {
|
||||||
let data = {}
|
let data = {}
|
||||||
try {
|
try {
|
||||||
data = {
|
data = {
|
||||||
cpuInfo: await cpuInfo(),
|
cpuInfo: await cpuInfo(),
|
||||||
memInfo: await memInfo(),
|
memInfo: await memInfo(),
|
||||||
driveInfo: await driveInfo(),
|
driveInfo: await driveInfo(),
|
||||||
netstatInfo: await netstatInfo(),
|
netstatInfo: await netstatInfo(),
|
||||||
osInfo: await osInfo(),
|
osInfo: await osInfo(),
|
||||||
openedCount: await openedCount()
|
openedCount: await openedCount()
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
} catch(err){
|
} catch(err){
|
||||||
console.error('获取系统信息出错:', err)
|
console.error('获取系统信息出错:', err)
|
||||||
return err.toString()
|
return err.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
3
server/bin/www
Normal file
3
server/bin/www
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
console.log('start time', new Date())
|
||||||
|
require('../app/main.js')
|
@ -1,54 +1,57 @@
|
|||||||
{
|
{
|
||||||
"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": {
|
||||||
"outputPath": "dist",
|
"outputPath": "dist",
|
||||||
"scripts": "./*",
|
"scripts": "./*",
|
||||||
"assets": "./*"
|
"assets": "./*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"local": "cross-env EXEC_ENV=local nodemon ./app/main.js",
|
"local": "cross-env EXEC_ENV=local nodemon ./app/main.js",
|
||||||
"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": [],
|
},
|
||||||
"author": "",
|
"keywords": [],
|
||||||
"license": "ISC",
|
"author": "",
|
||||||
"nodemonConfig": {
|
"license": "ISC",
|
||||||
"ignore": [
|
"nodemonConfig": {
|
||||||
"*.json"
|
"ignore": [
|
||||||
]
|
"*.json"
|
||||||
},
|
]
|
||||||
"dependencies": {
|
},
|
||||||
"@koa/cors": "^3.1.0",
|
"dependencies": {
|
||||||
"axios": "^0.21.4",
|
"@koa/cors": "^3.1.0",
|
||||||
"is-ip": "^4.0.0",
|
"axios": "^0.21.4",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"crypto-js": "^4.1.1",
|
||||||
"koa": "^2.13.1",
|
"global": "^4.4.0",
|
||||||
"koa-body": "^4.2.0",
|
"is-ip": "^4.0.0",
|
||||||
"koa-compose": "^4.1.0",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"koa-compress": "^5.1.0",
|
"koa": "^2.13.1",
|
||||||
"koa-jwt": "^4.0.3",
|
"koa-body": "^4.2.0",
|
||||||
"koa-router": "^10.0.0",
|
"koa-compose": "^4.1.0",
|
||||||
"koa-sslify": "^5.0.0",
|
"koa-compress": "^5.1.0",
|
||||||
"koa-static": "^5.0.0",
|
"koa-jwt": "^4.0.3",
|
||||||
"koa2-connect-history-api-fallback": "^0.1.3",
|
"koa-router": "^10.0.0",
|
||||||
"log4js": "^6.4.4",
|
"koa-sslify": "^5.0.0",
|
||||||
"node-os-utils": "^1.3.6",
|
"koa-static": "^5.0.0",
|
||||||
"node-rsa": "^1.1.1",
|
"koa2-connect-history-api-fallback": "^0.1.3",
|
||||||
"node-schedule": "^2.1.0",
|
"log4js": "^6.4.4",
|
||||||
"socket.io": "^4.4.1",
|
"node-os-utils": "^1.3.6",
|
||||||
"socket.io-client": "^4.5.1",
|
"node-rsa": "^1.1.1",
|
||||||
"ssh2": "^1.10.0"
|
"node-schedule": "^2.1.0",
|
||||||
},
|
"socket.io": "^4.4.1",
|
||||||
"devDependencies": {
|
"socket.io-client": "^4.5.1",
|
||||||
"cross-env": "^7.0.3",
|
"ssh2": "^1.10.0"
|
||||||
"eslint": "^7.32.0",
|
},
|
||||||
"nodemon": "^2.0.15",
|
"devDependencies": {
|
||||||
"pkg": "5.6"
|
"cross-env": "^7.0.3",
|
||||||
}
|
"eslint": "^7.32.0",
|
||||||
}
|
"nodemon": "^2.0.15",
|
||||||
|
"pkg": "5.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
6055
server/yarn.lock
6055
server/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user