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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

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 = [ // 注意注册顺序
compress, module.exports = [
history, compress,
useStatic, history,
useCors, useStatic, // staic先注册不然会被jwt拦截
responseHandler, useCors,
useKoaBody, responseHandler,
useLog, useKoaBody, // 先处理bodylog和router都要用到
useJwt, useLog, // 日志记录开始【该module使用到了fs.mkdir()等读写api 设置保存日志的目录需使用process.cwd()】
useAllowedMethods, useAuth,
useRoutes useAllowedMethods,
] 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,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

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

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

View File

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

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

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

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

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,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}|:((:[\dafAF]1,4)1,6|:)|:((:[\dafAF]1,4)1,6|:)|^[\da-fA-F]{1,4}:((:[\da-fA-F]{1,4}){1,5}|:)|([\dafAF]1,4:)2((:[\dafAF]1,4)1,4|:)|([\dafAF]1,4:)2((:[\dafAF]1,4)1,4|:)|^([\da-fA-F]{1,4}:){3}((:[\da-fA-F]{1,4}){1,3}|:)|([\dafAF]1,4:)4((:[\dafAF]1,4)1,2|:)|([\dafAF]1,4:)4((:[\dafAF]1,4)1,2|:)|^([\da-fA-F]{1,4}:){5}:([\da-fA-F]{1,4})?|([\dafAF]1,4:)6:|([\dafAF]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}|:((:[\dafAF]1,4)1,6|:)|:((:[\dafAF]1,4)1,6|:)|^[\da-fA-F]{1,4}:((:[\da-fA-F]{1,4}){1,5}|:)|([\dafAF]1,4:)2((:[\dafAF]1,4)1,4|:)|([\dafAF]1,4:)2((:[\dafAF]1,4)1,4|:)|^([\da-fA-F]{1,4}:){3}((:[\da-fA-F]{1,4}){1,3}|:)|([\dafAF]1,4:)4((:[\dafAF]1,4)1,2|:)|([\dafAF]1,4:)4((:[\dafAF]1,4)1,2|:)|^([\da-fA-F]{1,4}:){5}:([\da-fA-F]{1,4})?|([\dafAF]1,4:)6:|([\dafAF]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
} }

View File

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

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

View File

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

File diff suppressed because it is too large Load Diff

6055
yarn.lock

File diff suppressed because it is too large Load Diff