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 schedule = require('node-schedule')
const axios = require('axios')
let getOsData = require('../utils/os-data')
let serverSockets = {}, ipInfo = {}, osData = {}
async function getIpInfo() {
try {
let { data } = await axios.get('http://ip-api.com/json?lang=zh-CN')
console.log('getIpInfo Success: ', new Date())
ipInfo = data
} catch (error) {
console.log('getIpInfo Error: ', new Date(), error)
}
}
function ipSchedule() {
let rule1 = new schedule.RecurrenceRule()
rule1.second = [0, 10, 20, 30, 40, 50]
schedule.scheduleJob(rule1, () => {
let { query, country, city } = ipInfo || {}
if(query && country && city) return
console.log('Task: start getIpInfo', new Date())
getIpInfo()
})
// 每日凌晨两点整,刷新ip信息(兼容动态ip服务器)
let rule2 = new schedule.RecurrenceRule()
rule2.hour = 2
rule2.minute = 0
rule2.second = 0
schedule.scheduleJob(rule2, () => {
console.log('Task: refresh ip info', new Date())
getIpInfo()
})
}
ipSchedule()
module.exports = (httpServer) => {
const serverIo = new Server(httpServer, {
path: '/client/os-info',
cors: {
origin: '*'
}
})
serverIo.on('connection', (socket) => {
serverSockets[socket.id] = setInterval(async () => {
try {
osData = await getOsData()
socket && socket.emit('client_data', Object.assign(osData, { ipInfo }))
} catch (error) {
console.error('客户端错误:', error)
socket && socket.emit('client_error', { error })
}
}, 1500)
socket.on('disconnect', () => {
if(serverSockets[socket.id]) clearInterval(serverSockets[socket.id])
delete serverSockets[socket.id]
socket.close && socket.close()
socket = null
})
})
}
const { Server } = require('socket.io')
const schedule = require('node-schedule')
const axios = require('axios')
let getOsData = require('../utils/os-data')
let serverSockets = {}, ipInfo = {}, osData = {}
async function getIpInfo() {
try {
let { data } = await axios.get('http://ip-api.com/json?lang=zh-CN')
console.log('getIpInfo Success: ', new Date())
ipInfo = data
} catch (error) {
console.log('getIpInfo Error: ', new Date(), error)
}
}
function ipSchedule() {
let rule1 = new schedule.RecurrenceRule()
rule1.second = [0, 10, 20, 30, 40, 50]
schedule.scheduleJob(rule1, () => {
let { query, country, city } = ipInfo || {}
if(query && country && city) return
console.log('Task: start getIpInfo', new Date())
getIpInfo()
})
// 每日凌晨两点整,刷新ip信息(兼容动态ip服务器)
let rule2 = new schedule.RecurrenceRule()
rule2.hour = 2
rule2.minute = 0
rule2.second = 0
schedule.scheduleJob(rule2, () => {
console.log('Task: refresh ip info', new Date())
getIpInfo()
})
}
ipSchedule()
module.exports = (httpServer) => {
const serverIo = new Server(httpServer, {
path: '/client/os-info',
cors: {
origin: '*'
}
})
serverIo.on('connection', (socket) => {
serverSockets[socket.id] = setInterval(async () => {
try {
osData = await getOsData()
socket && socket.emit('client_data', Object.assign(osData, { ipInfo }))
} catch (error) {
console.error('客户端错误:', error)
socket && socket.emit('client_error', { error })
}
}, 1500)
socket.on('disconnect', () => {
if(serverSockets[socket.id]) clearInterval(serverSockets[socket.id])
delete serverSockets[socket.id]
socket.close && socket.close()
socket = null
})
})
}

View File

@ -1,83 +1,83 @@
const osu = require('node-os-utils')
const os = require('os')
let cpu = osu.cpu
let mem = osu.mem
let drive = osu.drive
let netstat = osu.netstat
let osuOs = osu.os
let users = osu.users
async function cpuInfo() {
let cpuUsage = await cpu.usage(200)
let cpuCount = cpu.count()
let cpuModel = cpu.model()
return {
cpuUsage,
cpuCount,
cpuModel
}
}
async function memInfo() {
let memInfo = await mem.info()
return {
...memInfo
}
}
async function driveInfo() {
let driveInfo = {}
try {
driveInfo = await drive.info()
} catch {
// console.log(driveInfo)
}
return driveInfo
}
async function netstatInfo() {
let netstatInfo = await netstat.inOut()
return netstatInfo === 'not supported' ? {} : netstatInfo
}
async function osInfo() {
let type = os.type()
let platform = os.platform()
let release = os.release()
let uptime = osuOs.uptime()
let ip = osuOs.ip()
let hostname = osuOs.hostname()
let arch = osuOs.arch()
return {
type,
platform,
release,
ip,
hostname,
arch,
uptime
}
}
async function openedCount() {
let openedCount = await users.openedCount()
return openedCount === 'not supported' ? 0 : openedCount
}
module.exports = async () => {
let data = {}
try {
data = {
cpuInfo: await cpuInfo(),
memInfo: await memInfo(),
driveInfo: await driveInfo(),
netstatInfo: await netstatInfo(),
osInfo: await osInfo(),
openedCount: await openedCount()
}
return data
} catch(err){
return err.toString()
}
}
const osu = require('node-os-utils')
const os = require('os')
let cpu = osu.cpu
let mem = osu.mem
let drive = osu.drive
let netstat = osu.netstat
let osuOs = osu.os
let users = osu.users
async function cpuInfo() {
let cpuUsage = await cpu.usage(200)
let cpuCount = cpu.count()
let cpuModel = cpu.model()
return {
cpuUsage,
cpuCount,
cpuModel
}
}
async function memInfo() {
let memInfo = await mem.info()
return {
...memInfo
}
}
async function driveInfo() {
let driveInfo = {}
try {
driveInfo = await drive.info()
} catch {
// console.log(driveInfo)
}
return driveInfo
}
async function netstatInfo() {
let netstatInfo = await netstat.inOut()
return netstatInfo === 'not supported' ? {} : netstatInfo
}
async function osInfo() {
let type = os.type()
let platform = os.platform()
let release = os.release()
let uptime = osuOs.uptime()
let ip = osuOs.ip()
let hostname = osuOs.hostname()
let arch = osuOs.arch()
return {
type,
platform,
release,
ip,
hostname,
arch,
uptime
}
}
async function openedCount() {
let openedCount = await users.openedCount()
return openedCount === 'not supported' ? 0 : openedCount
}
module.exports = async () => {
let data = {}
try {
data = {
cpuInfo: await cpuInfo(),
memInfo: await memInfo(),
driveInfo: await driveInfo(),
netstatInfo: await netstatInfo(),
osInfo: await osInfo(),
openedCount: await openedCount()
}
return data
} catch(err){
return err.toString()
}
}

View File

@ -1,13 +1,13 @@
FROM node:16.15.0-alpine3.14
ARG TARGET_DIR=/easynode-server
WORKDIR ${TARGET_DIR}
RUN yarn config set registry https://registry.npm.taobao.org
COPY package.json ${TARGET_DIR}
COPY yarn.lock ${TARGET_DIR}
RUN yarn
COPY . ${TARGET_DIR}
ENV HOST 0.0.0.0
EXPOSE 8082
EXPOSE 8083
EXPOSE 22022
CMD ["npm", "run", "server"]
FROM node:16.15.0-alpine3.14
ARG TARGET_DIR=/easynode-server
WORKDIR ${TARGET_DIR}
RUN yarn config set registry https://registry.npm.taobao.org
COPY package.json ${TARGET_DIR}
COPY yarn.lock ${TARGET_DIR}
RUN yarn
COPY . ${TARGET_DIR}
ENV HOST 0.0.0.0
EXPOSE 8082
EXPOSE 8083
EXPOSE 22022
CMD ["npm", "run", "server"]

View File

@ -1,30 +1,30 @@
const path = require('path')
const fs = require('fs')
const getCertificate =() => {
try {
return {
cert: fs.readFileSync(path.join(__dirname, './pem/cert.pem')),
key: fs.readFileSync(path.join(__dirname, './pem/key.pem'))
}
} catch (error) {
return null
}
}
module.exports = {
domain: 'yourDomain', // 域名xxx.com
httpPort: 8082,
httpsPort: 8083,
clientPort: 22022, // 勿更改
certificate: getCertificate(),
uploadDir: path.join(process.cwd(),'./app/static/upload'),
staticDir: path.join(process.cwd(),'./app/static'),
sshRecordPath: path.join(__dirname,'./storage/ssh-record.json'),
keyPath: path.join(__dirname,'./storage/key.json'),
hostListPath: path.join(__dirname,'./storage/host-list.json'),
apiPrefix: '/api/v1',
logConfig: {
outDir: path.join(process.cwd(),'./app/logs'),
flag: false // 是否记录日志
}
}
const path = require('path')
const fs = require('fs')
const getCertificate =() => {
try {
return {
cert: fs.readFileSync(path.join(__dirname, './pem/cert.pem')),
key: fs.readFileSync(path.join(__dirname, './pem/key.pem'))
}
} catch (error) {
return null
}
}
module.exports = {
domain: '', // 域名(必须配置, 跨域使用[不配置将所有域名可访问api])
httpPort: 8082,
httpsPort: 8083,
clientPort: 22022, // 勿更改
certificate: getCertificate(),
uploadDir: path.join(process.cwd(),'./app/static/upload'),
staticDir: path.join(process.cwd(),'./app/static'),
sshRecordPath: path.join(__dirname,'./storage/ssh-record.json'),
keyPath: path.join(__dirname,'./storage/key.json'),
hostListPath: path.join(__dirname,'./storage/host-list.json'),
apiPrefix: '/api/v1',
logConfig: {
outDir: path.join(process.cwd(),'./app/logs'),
flag: false // 是否记录日志
}
}

View File

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

View File

@ -1,61 +1,68 @@
const { readHostList, writeHostList } = require('../utils')
function getHostList({ res }) {
const data = readHostList()
res.success({ data })
}
function saveHost({ res, request }) {
let { body: { host: newHost, name } } = request
if(!newHost || !name) return res.fail({ msg: '参数错误' })
let hostList = readHostList()
if(hostList.some(({ host }) => host === newHost)) return res.fail({ msg: `主机${ newHost }已存在` })
hostList.push({ host: newHost, name })
writeHostList(hostList)
res.success()
}
function updateHost({ res, request }) {
let { body: { host: newHost, name: newName, oldHost } } = request
if(!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' })
let hostList = readHostList()
if(!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `主机${ newHost }不存在` })
let targetIdx = hostList.findIndex(({ host }) => host === oldHost)
hostList.splice(targetIdx, 1, { name: newName, host: newHost })
writeHostList(hostList)
res.success()
}
function removeHost({ res, request }) {
let { body: { host } } = request
let hostList = readHostList()
let hostIdx = hostList.findIndex(item => item.host === host)
if(hostIdx === -1) return res.fail({ msg: `${ host }不存在` })
hostList.splice(hostIdx, 1)
writeHostList(hostList)
res.success({ data: `${ host }已移除` })
}
function updateHostSort({ res, request }) {
let { body: { list } } = request
if(!list) return res.fail({ msg: '参数错误' })
let hostList = readHostList()
if(hostList.length !== list.length) return res.fail({ msg: '失败: host数量不匹配' })
let sortResult = []
for (let i = 0; i < list.length; i++) {
const curHost = list[i]
let temp = hostList.find(({ host }) => curHost.host === host)
if(!temp) return res.fail({ msg: `查找失败: ${ curHost.name }` })
sortResult.push(temp)
}
writeHostList(sortResult)
res.success({ msg: 'success' })
}
module.exports = {
getHostList,
saveHost,
updateHost,
removeHost,
updateHostSort
}
const { readHostList, writeHostList, readSSHRecord, writeSSHRecord } = require('../utils')
function getHostList({ res }) {
const data = readHostList()
res.success({ data })
}
function saveHost({ res, request }) {
let { body: { host: newHost, name } } = request
if(!newHost || !name) return res.fail({ msg: '参数错误' })
let hostList = readHostList()
if(hostList.some(({ host }) => host === newHost)) return res.fail({ msg: `主机${ newHost }已存在` })
hostList.push({ host: newHost, name })
writeHostList(hostList)
res.success()
}
function updateHost({ res, request }) {
let { body: { host: newHost, name: newName, oldHost } } = request
if(!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' })
let hostList = readHostList()
if(!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `主机${ newHost }不存在` })
let targetIdx = hostList.findIndex(({ host }) => host === oldHost)
hostList.splice(targetIdx, 1, { name: newName, host: newHost })
writeHostList(hostList)
res.success()
}
function removeHost({ res, request }) {
let { body: { host } } = request
let hostList = readHostList()
let hostIdx = hostList.findIndex(item => item.host === host)
if(hostIdx === -1) return res.fail({ msg: `${ host }不存在` })
hostList.splice(hostIdx, 1)
writeHostList(hostList)
// 查询是否存在ssh记录
let sshRecord = readSSHRecord()
let sshIdx = sshRecord.findIndex(item => item.host === host)
let flag = sshIdx !== -1
if(flag) sshRecord.splice(sshIdx, 1)
writeSSHRecord(sshRecord)
res.success({ data: `${ host }已移除, ${ flag ? '并移除ssh记录' : '' }` })
}
function updateHostSort({ res, request }) {
let { body: { list } } = request
if(!list) return res.fail({ msg: '参数错误' })
let hostList = readHostList()
if(hostList.length !== list.length) return res.fail({ msg: '失败: host数量不匹配' })
let sortResult = []
for (let i = 0; i < list.length; i++) {
const curHost = list[i]
let temp = hostList.find(({ host }) => curHost.host === host)
if(!temp) return res.fail({ msg: `查找失败: ${ curHost.name }` })
sortResult.push(temp)
}
writeHostList(sortResult)
res.success({ msg: 'success' })
}
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 updateSSH = async ({ res, request }) => {
let { body: { host, port, username, type, password, privateKey, command } } = request
let record = { host, port, username, type, password, privateKey, command }
let sshRecord = readSSHRecord()
let idx = sshRecord.findIndex(item => item.host === host)
if(idx === -1)
sshRecord.push(record)
else
sshRecord.splice(idx, 1, record)
writeSSHRecord(sshRecord)
res.success({ data: '保存成功' })
}
const removeSSH = async ({ res, request }) => {
let { body: { host } } = request
let sshRecord = readSSHRecord()
let idx = sshRecord.findIndex(item => item.host === host)
if(idx === -1) return res.fail({ msg: '凭证不存在' })
sshRecord.splice(idx, 1)
writeSSHRecord(sshRecord)
res.success({ data: '移除成功' })
}
const existSSH = async ({ res, request }) => {
let { body: { host } } = request
let sshRecord = readSSHRecord()
let idx = sshRecord.findIndex(item => item.host === host)
if(idx === -1) return res.success({ data: false })
res.success({ data: true })
}
const getCommand = async ({ res, request }) => {
let { host } = request.query
if(!host) return res.fail({ data: false, msg: '参数错误' })
let sshRecord = readSSHRecord()
let record = sshRecord.find(item => item.host === host)
if(!record) return res.fail({ data: false, msg: 'host not found' })
const { command } = record
if(!command) return res.success({ data: false })
res.success({ data: command })
}
module.exports = {
updateSSH,
removeSSH,
existSSH,
getCommand
}
const { readSSHRecord, writeSSHRecord, AESEncrypt } = require('../utils')
const updateSSH = async ({ res, request }) => {
let { body: { host, port, username, type, password, privateKey, randomKey, command } } = request
let record = { host, port, username, type, password, privateKey, randomKey, command }
if(!host || !port || !username || !type || !randomKey) return res.fail({ data: false, msg: '参数错误' })
// 再做一次对称加密(方便ssh连接时解密)
record.randomKey = AESEncrypt(randomKey)
let sshRecord = readSSHRecord()
let idx = sshRecord.findIndex(item => item.host === host)
if(idx === -1)
sshRecord.push(record)
else
sshRecord.splice(idx, 1, record)
writeSSHRecord(sshRecord)
console.log('新增凭证:', host)
res.success({ data: '保存成功' })
}
const removeSSH = async ({ res, request }) => {
let { body: { host } } = request
let sshRecord = readSSHRecord()
let idx = sshRecord.findIndex(item => item.host === host)
if(idx === -1) return res.fail({ msg: '凭证不存在' })
sshRecord.splice(idx, 1)
console.log('移除凭证:', host)
writeSSHRecord(sshRecord)
res.success({ data: '移除成功' })
}
const existSSH = async ({ res, request }) => {
let { body: { host } } = request
let sshRecord = readSSHRecord()
let idx = sshRecord.findIndex(item => item.host === host)
console.log('查询凭证:', host)
if(idx === -1) return res.success({ data: false }) // host不存在
res.success({ data: true }) // 存在
}
const getCommand = async ({ res, request }) => {
let { host } = request.query
if(!host) return res.fail({ data: false, msg: '参数错误' })
let sshRecord = readSSHRecord()
let record = sshRecord.find(item => item.host === host)
console.log('查询登录后执行的指令:', host)
if(!record) return res.fail({ data: false, msg: 'host not found' }) // host不存在
const { command } = record
if(!command) return res.success({ data: false }) // command不存在
res.success({ data: command }) // 存在
}
module.exports = {
updateSSH,
removeSSH,
existSSH,
getCommand
}

View File

@ -1,39 +1,66 @@
const jwt = require('jsonwebtoken')
const { readKey, writeKey, decrypt } = require('../utils')
const getpublicKey = ({ res }) => {
let { publicKey: data } = readKey()
if(!data) return res.fail({ msg: 'publicKey not found, Try to restart the server', status: 500 })
res.success({ data })
}
const login = async ({ res, request }) => {
let { body: { ciphertext } } = request
if(!ciphertext) return res.fail({ msg: '参数错误' })
try {
const password = decrypt(ciphertext)
let { pwd, jwtSecret, jwtExpires } = readKey()
if(password !== pwd) return res.fail({ msg: '密码错误' })
const token = jwt.sign({ date: Date.now() }, jwtSecret, { expiresIn: jwtExpires }) // 生成token
res.success({ data: { token, jwtExpires } })
} catch (error) {
res.fail({ msg: '解密失败' })
}
}
const updatePwd = async ({ res, request }) => {
let { body: { oldPwd, newPwd } } = request
oldPwd = decrypt(oldPwd)
newPwd = decrypt(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' })
}
module.exports = {
login,
getpublicKey,
updatePwd
}
const jwt = require('jsonwebtoken')
const { getNetIPInfo, readKey, writeKey, RSADecrypt, AESEncrypt, SHA1Encrypt } = require('../utils')
const getpublicKey = ({ res }) => {
let { publicKey: data } = readKey()
if(!data) return res.fail({ msg: 'publicKey not found, Try to restart the server', status: 500 })
res.success({ data })
}
const generateTokenAndRecordIP = async (clientIp) => {
console.log('密码校验成功, 准备生成token')
let { commonKey, jwtExpires } = readKey()
let token = jwt.sign({ date: Date.now() }, commonKey, { expiresIn: jwtExpires }) // 生成token
token = AESEncrypt(token) // 对称加密token后再传输给前端
console.log('aes对称加密token', token)
// 记录客户端登录IP用于判断是否异地(只保留最近10条)
const localNetIPInfo = await getNetIPInfo(clientIp)
global.loginRecord.unshift(localNetIPInfo)
if(global.loginRecord.length > 10) global.loginRecord = global.loginRecord.slice(0, 10)
return { token, jwtExpires }
}
const login = async ({ res, request }) => {
let { body: { ciphertext }, ip: clientIp } = request
if(!ciphertext) return res.fail({ msg: '参数错误' })
try {
console.log('ciphertext', ciphertext)
let password = RSADecrypt(ciphertext)
let { pwd } = readKey()
if(password === 'admin' && pwd === 'admin') {
const { token, jwtExpires } = await generateTokenAndRecordIP(clientIp)
return res.success({ data: { token, jwtExpires }, msg: '登录成功,请及时修改默认密码' })
}
password = SHA1Encrypt(password)
if(password !== pwd) return res.fail({ msg: '密码错误' })
const { token, jwtExpires } = await generateTokenAndRecordIP(clientIp)
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 NodeRSA = require('node-rsa')
async function initIp() {
if(!isProd()) return console.log('非生产环境不初始化保存本地IP')
const localNetIP = await getLocalNetIP()
let vpsList = readHostList()
if(vpsList.some(({ host }) => host === localNetIP)) return console.log('本机IP已储存: ', localNetIP)
vpsList.unshift({ name: 'server-side-host', host: localNetIP })
writeHostList(vpsList)
console.log('首次启动储存本机IP: ', localNetIP)
}
async function initRsa() {
let keyObj = readKey()
if(keyObj.privateKey && keyObj.publicKey) return console.log('公私钥已存在')
let key = new NodeRSA({ b: 1024 })
key.setOptions({ encryptionScheme: 'pkcs1' })
let privateKey = key.exportKey('pkcs1-private-pem')
let publicKey = key.exportKey('pkcs8-public-pem')
keyObj.privateKey = privateKey
keyObj.publicKey = publicKey
writeKey(keyObj)
console.log('新的公私钥已生成')
}
function randomJWTSecret() {
let keyObj = readKey()
if(keyObj.jwtSecret) return console.log('jwt secret已存在')
keyObj.jwtSecret = randomStr(32)
writeKey(keyObj)
console.log('已生成随机jwt secret')
}
module.exports = () => {
initIp()
initRsa()
randomJWTSecret()
}
const { getNetIPInfo, readHostList, writeHostList, readKey, writeKey, randomStr, isProd, AESEncrypt } = require('./utils')
const NodeRSA = require('node-rsa')
const isDev = !isProd()
// 存储本机IP, 供host列表接口调用
async function initIp() {
if(isDev) return console.log('非生产环境不初始化保存本地IP')
const localNetIPInfo = await getNetIPInfo()
let vpsList = readHostList()
let { ip: localNetIP } = localNetIPInfo
if(vpsList.some(({ host }) => host === localNetIP)) return console.log('本机IP已储存: ', localNetIP)
vpsList.unshift({ name: 'server-side-host', host: localNetIP })
writeHostList(vpsList)
console.log('Task: 生产环境首次启动储存本机IP: ', localNetIP)
}
// 初始化公私钥, 供登录、保存ssh密钥/密码等加解密
async function initRsa() {
let keyObj = readKey()
if(keyObj.privateKey && keyObj.publicKey) return console.log('公私钥已存在[重新生成会导致已保存的ssh密钥信息失效]')
let key = new NodeRSA({ b: 1024 })
key.setOptions({ encryptionScheme: 'pkcs1' })
let privateKey = key.exportKey('pkcs1-private-pem')
let publicKey = key.exportKey('pkcs8-public-pem')
keyObj.privateKey = AESEncrypt(privateKey) // 加密私钥
keyObj.publicKey = publicKey // 公开公钥
writeKey(keyObj)
console.log('Task: 已生成新的非对称加密公私钥')
}
// 随机的commonKey secret
function randomJWTSecret() {
let keyObj = readKey()
if(keyObj.commonKey) return console.log('commonKey密钥已存在')
keyObj.commonKey = randomStr(16)
writeKey(keyObj)
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 initLocal = require('./init')
initLocal()
httpServer()
httpsServer()
clientHttpServer()
const { httpServer, httpsServer, clientHttpServer } = require('./server')
const initLocal = require('./init')
initLocal()
httpServer()
httpsServer()
clientHttpServer()

View File

@ -1,24 +1,28 @@
const { verifyToken } = require('../utils')
const { apiPrefix } = require('../config')
let whitePath = [
'/login',
'/get-pub-pem'
].map(item => (apiPrefix + item))
const useJwt = async ({ request, res }, next) => {
const { path, headers: { token } } = request
if(whitePath.includes(path)) return next()
if(!token) return res.fail({ msg: '未登录', status: 403 })
const { code, msg } = verifyToken(token)
switch(code) {
case 1:
return await next()
case -1:
return res.fail({ msg, status: 401 })
case -2:
return res.fail({ msg: '登录态错误, 请重新登录', status: 401, data: msg })
}
}
module.exports = useJwt
const { verifyAuth } = require('../utils')
const { apiPrefix } = require('../config')
let whitePath = [
'/login',
'/get-pub-pem'
].map(item => (apiPrefix + item))
console.log('路由白名单:', whitePath)
const useAuth = async ({ request, res }, next) => {
const { path, headers: { token } } = request
console.log('path: ', path)
// console.log('token: ', token)
if(whitePath.includes(path)) return next()
if(!token) return res.fail({ msg: '未登录', status: 403 })
// 验证token
const { code, msg } = verifyAuth(token, request.ip)
switch(code) {
case 1:
return await next()
case -1:
return res.fail({ msg, status: 401 })
case -2:
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')
module.exports = koaBody({
multipart: true,
multipart: true, // 支持 multipart-formdate 的表单
formidable: {
uploadDir,
keepExtensions: true,
multipart: true,
maxFieldsSize: 2 * 1024 * 1024
uploadDir, // 上传目录
keepExtensions: true, // 保持文件的后缀
multipart: true, // 多文件上传
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 options = { threshold: 2048 }
module.exports = compress(options)
const compress = require('koa-compress')
const options = { threshold: 2048 }
module.exports = compress(options)

View File

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

View File

@ -1,3 +1,3 @@
const { historyApiFallback } = require('koa2-connect-history-api-fallback')
module.exports = historyApiFallback({ whiteList: ['/api'] })
const { historyApiFallback } = require('koa2-connect-history-api-fallback')
module.exports = historyApiFallback({ whiteList: ['/api'] })

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,13 @@
const { apiPrefix } = require('../config')
const koaRouter = require('koa-router')
const router = new koaRouter({ prefix: apiPrefix })
const routeList = require('./routes')
routeList.forEach(item => {
const { method, path, controller } = item
router[method](path, controller)
})
module.exports = router
const { apiPrefix } = require('../config')
const koaRouter = require('koa-router')
const router = new koaRouter({ prefix: apiPrefix })
const routeList = require('./routes')
// 统一注册路由
routeList.forEach(item => {
const { method, path, controller } = item
router[method](path, controller)
})
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 { getHostList, saveHost, updateHost, removeHost, updateHostSort } = require('../controller/host-info')
const { login, getpublicKey, updatePwd } = require('../controller/user')
const routes = [
{
method: 'get',
path: '/os-info',
controller: osInfo
},
{
method: 'post',
path: '/update-ssh',
controller: updateSSH
},
{
method: 'post',
path: '/remove-ssh',
controller: removeSSH
},
{
method: 'post',
path: '/exist-ssh',
controller: existSSH
},
{
method: 'get',
path: '/command',
controller: getCommand
},
{
method: 'get',
path: '/host-list',
controller: getHostList
},
{
method: 'post',
path: '/host-save',
controller: saveHost
},
{
method: 'put',
path: '/host-save',
controller: updateHost
},
{
method: 'post',
path: '/host-remove',
controller: removeHost
},
{
method: 'put',
path: '/host-sort',
controller: updateHostSort
},
{
method: 'get',
path: '/get-pub-pem',
controller: getpublicKey
},
{
method: 'post',
path: '/login',
controller: login
},
{
method: 'put',
path: '/pwd',
controller: updatePwd
}
]
module.exports = routes
const { updateSSH, removeSSH, existSSH, getCommand } = require('../controller/ssh-info')
const { getHostList, saveHost, updateHost, removeHost, updateHostSort } = require('../controller/host-info')
const { login, getpublicKey, updatePwd, getLoginRecord } = require('../controller/user')
// 路由统一管理
const routes = [
{
method: 'post',
path: '/update-ssh',
controller: updateSSH
},
{
method: 'post',
path: '/remove-ssh',
controller: removeSSH
},
{
method: 'post',
path: '/exist-ssh',
controller: existSSH
},
{
method: 'get',
path: '/command',
controller: getCommand
},
{
method: 'get',
path: '/host-list',
controller: getHostList
},
{
method: 'post',
path: '/host-save',
controller: saveHost
},
{
method: 'put',
path: '/host-save',
controller: updateHost
},
{
method: 'post',
path: '/host-remove',
controller: removeHost
},
{
method: 'put',
path: '/host-sort',
controller: updateHostSort
},
{
method: 'get',
path: '/get-pub-pem',
controller: getpublicKey
},
{
method: 'post',
path: '/login',
controller: login
},
{
method: 'put',
path: '/pwd',
controller: updatePwd
},
{
method: 'get',
path: '/get-login-record',
controller: getLoginRecord
}
]
module.exports = routes

View File

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

View File

@ -1,73 +1,96 @@
const { Server: ServerIO } = require('socket.io')
const { io: ClientIO } = require('socket.io-client')
const { readHostList } = require('../utils')
const { clientPort } = require('../config')
const { verifyToken } = require('../utils')
let clientSockets = {}, clientsData = {}, timer = null
function getClientsInfo(socketId) {
let hostList = readHostList()
hostList
.map(({ host }) => {
let clientSocket = ClientIO(`http://${ host }:${ clientPort }`, {
path: '/client/os-info',
forceNew: true,
reconnectionDelay: 3000,
reconnectionAttempts: 1
})
clientSockets[socketId].push(clientSocket)
return {
host,
clientSocket
}
})
.map(({ host, clientSocket }) => {
clientSocket
.on('connect', () => {
clientSocket.on('client_data', (osData) => {
clientsData[host] = osData
})
clientSocket.on('client_error', (error) => {
clientsData[host] = error
})
})
.on('connect_error', () => {
clientsData[host] = null
})
.on('disconnect', () => {
clientsData[host] = null
})
})
}
module.exports = (httpServer) => {
const serverIo = new ServerIO(httpServer, {
path: '/clients',
cors: {
}
})
serverIo.on('connection', (socket) => {
socket.on('init_clients_data', ({ token }) => {
const { code } = verifyToken(token)
if(code !== 1) return socket.emit('token_verify_fail', 'token无效')
clientSockets[socket.id] = []
getClientsInfo(socket.id)
socket.emit('clients_data', clientsData)
timer = setInterval(() => {
socket.emit('clients_data', clientsData)
}, 1500)
socket.on('disconnect', () => {
if(timer) clearInterval(timer)
clientSockets[socket.id].forEach(socket => socket.close && socket.close())
delete clientSockets[socket.id]
})
})
})
}
const { Server: ServerIO } = require('socket.io')
const { io: ClientIO } = require('socket.io-client')
const { readHostList } = require('../utils')
const { clientPort } = require('../config')
const { verifyAuth } = require('../utils')
let clientSockets = {}, clientsData = {}
function getClientsInfo(socketId) {
let hostList = readHostList()
hostList
.map(({ host }) => {
let clientSocket = ClientIO(`http://${ host }:${ clientPort }`, {
path: '/client/os-info',
forceNew: true,
timeout: 5000,
reconnectionDelay: 3000,
reconnectionAttempts: 100
})
// 将与客户端连接的socket实例保存起来web端断开时关闭这些连接
clientSockets[socketId].push(clientSocket)
return {
host,
clientSocket
}
})
.map(({ host, clientSocket }) => {
clientSocket
.on('connect', () => {
console.log('client connect success:', host)
clientSocket.on('client_data', (osData) => {
clientsData[host] = osData
})
clientSocket.on('client_error', (error) => {
clientsData[host] = error
})
})
.on('connect_error', (error) => {
console.log('client connect fail:', host, error.message)
clientsData[host] = null
})
.on('disconnect', () => {
console.log('client connect disconnect:', host)
clientsData[host] = null
})
})
}
module.exports = (httpServer) => {
const serverIo = new ServerIO(httpServer, {
path: '/clients',
cors: {
origin: '*' // 需配置跨域
}
})
serverIo.on('connection', (socket) => {
// 前者兼容nginx反代, 后者兼容nodejs自身服务
let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
socket.on('init_clients_data', ({ token }) => {
// 校验登录态
const { code, msg } = verifyAuth(token, clientIp)
if(code !== 1) {
socket.emit('token_verify_fail', msg || '鉴权失败')
socket.disconnect()
return
}
// 收集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()
})
// 每日凌晨两点整,刷新ip信息(兼容动态ip服务器)
let rule2 = new schedule.RecurrenceRule()
rule2.hour = 2
rule2.minute = 0
@ -46,6 +47,7 @@ module.exports = (httpServer) => {
})
serverIo.on('connection', (socket) => {
// 存储对应websocket连接的定时器
serverSockets[socket.id] = setInterval(async () => {
try {
osData = await getOsData()
@ -54,13 +56,15 @@ module.exports = (httpServer) => {
console.error('客户端错误:', error)
socket && socket.emit('client_error', { error })
}
}, 1500)
}, 1000)
socket.on('disconnect', () => {
// 断开时清楚对应的websocket连接
if(serverSockets[socket.id]) clearInterval(serverSockets[socket.id])
delete serverSockets[socket.id]
socket.close && socket.close()
socket = null
// console.log('断开socketId: ', socket.id, '剩余链接数: ', Object.keys(serverSockets).length)
})
})
}

View File

@ -1,71 +1,92 @@
const { Server } = require('socket.io')
const { Client: Client } = require('ssh2')
const { readSSHRecord, verifyToken } = require('../utils')
function createTerminal(socket, vps) {
vps.shell({ term: 'xterm-color', cols: 100, rows: 30 }, (err, stream) => {
if (err) return socket.emit('output', err.toString())
stream
.on('data', (data) => {
socket.emit('output', data.toString())
})
.on('close', () => {
vps.end()
})
socket.on('input', key => {
if(vps._sock.writable === false) return console.log('终端连接已关闭')
stream.write(key)
})
socket.emit('connect_terminal')
})
}
module.exports = (httpServer) => {
const serverIo = new Server(httpServer, {
path: '/terminal',
cors: {
origin: '*'
}
})
serverIo.on('connection', (socket) => {
let vps = new Client()
socket.on('create', ({ host: ip, token }) => {
const { code } = verifyToken(token)
if(code !== 1) return socket.emit('token_verify_fail')
const sshRecord = readSSHRecord()
let loginInfo = sshRecord.find(item => item.host === ip)
if(!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ ip }】凭证`)
const { type, host, port, username } = loginInfo
try {
vps
.on('ready', () => {
socket.emit('connect_success', `已连接到服务器:${ host }`)
createTerminal(socket, vps)
})
.on('error', (err) => {
socket.emit('connect_fail', err.message)
})
.connect({
type: 'privateKey',
host,
port,
username,
[type]: loginInfo[type]
})
} catch (err) {
socket.emit('create_fail', err.message)
}
})
socket.on('disconnect', () => {
vps.end()
vps.destroy()
vps = null
})
})
}
const { Server } = require('socket.io')
const { Client: Client } = require('ssh2')
const { readSSHRecord, verifyAuth, RSADecrypt, AESDecrypt } = require('../utils')
function createTerminal(socket, vps) {
vps.shell({ term: 'xterm-color' }, (err, stream) => {
if (err) return socket.emit('output', err.toString())
stream
.on('data', (data) => {
socket.emit('output', data.toString())
})
.on('close', () => {
console.log('关闭终端')
vps.end()
})
socket.on('input', key => {
if(vps._sock.writable === false) return console.log('终端连接已关闭')
stream.write(key)
})
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: '*'
}
})
serverIo.on('connection', (socket) => {
// 前者兼容nginx反代, 后者兼容nodejs自身服务
let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
let vps = new Client()
console.log('terminal websocket 已连接')
socket.on('create', ({ host: ip, token }) => {
const { code } = verifyAuth(token, clientIp)
if(code !== 1) {
socket.emit('token_verify_fail')
socket.disconnect()
return
}
// console.log('code:', code)
const sshRecord = readSSHRecord()
let loginInfo = sshRecord.find(item => item.host === ip)
if(!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ ip }】凭证`)
let { type, host, port, username, randomKey } = loginInfo
try {
// 解密放到try里面防止报错【公私钥必须配对, 否则需要重新添加服务器密钥】
randomKey = AESDecrypt(randomKey) // 先对称解密key
randomKey = RSADecrypt(randomKey) // 再非对称解密key
loginInfo[type] = AESDecrypt(loginInfo[type], randomKey) // 对称解密ssh密钥
console.log('准备连接服务器:', host)
vps
.on('ready', () => {
console.log('已连接到服务器:', host)
socket.emit('connect_success', `已连接到服务器:${ host }`)
createTerminal(socket, vps)
})
.on('error', (err) => {
console.log('连接失败:', err.level)
socket.emit('connect_fail', err.message)
})
.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">
<title>EasyNode</title>
<script src="//at.alicdn.com/t/font_3309550_eg9tjmfmiku.js"></script>
<script type="module" crossorigin src="/assets/index.49bfeae7.js"></script>
<link rel="stylesheet" href="/assets/index.4226ec12.css">
<script type="module" crossorigin src="/assets/index.704cb447.js"></script>
<link rel="stylesheet" href="/assets/index.fdac59aa.css">
</head>
<body>
<div id="app" style="opacity: 0.9;"></div>
<div id="app"></div>
</body>
</html>

View File

@ -1,120 +1,189 @@
const fs = require('fs')
const jwt = require('jsonwebtoken')
const axios = require('axios')
const NodeRSA = require('node-rsa')
const { sshRecordPath, hostListPath, keyPath } = require('../config')
const readSSHRecord = () => {
let list
try {
list = JSON.parse(fs.readFileSync(sshRecordPath, 'utf8'))
} catch (error) {
writeSSHRecord([])
}
return list || []
}
const writeSSHRecord = (record = []) => {
fs.writeFileSync(sshRecordPath, JSON.stringify(record, null, 2))
}
const readHostList = () => {
let list
try {
list = JSON.parse(fs.readFileSync(hostListPath, 'utf8'))
} catch (error) {
writeHostList([])
}
return list || []
}
const writeHostList = (record = []) => {
fs.writeFileSync(hostListPath, JSON.stringify(record, null, 2))
}
const readKey = () => {
let keyObj = JSON.parse(fs.readFileSync(keyPath, 'utf8'))
return keyObj
}
const writeKey = (keyObj = {}) => {
fs.writeFileSync(keyPath, JSON.stringify(keyObj, null, 2))
}
const getLocalNetIP = async () => {
try {
let ipUrls = ['http://ip-api.com/json/?lang=zh-CN', 'http://whois.pconline.com.cn/ipJson.jsp?json=true']
let { data } = await Promise.race(ipUrls.map(url => axios.get(url)))
return data.ip || data.query
} catch (error) {
console.error('getIpInfo Error: ', error)
return {
ip: '未知',
country: '未知',
city: '未知',
error
}
}
}
const throwError = ({ status = 500, msg = 'defalut error' } = {}) => {
const err = new Error(msg)
err.status = status
throw err
}
const isIP = (ip = '') => {
const isIPv4 = /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/
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:/
return isIPv4.test(ip) || isIPv6.test(ip)
}
const randomStr = (e) =>{
e = e || 32
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 verifyToken = (token) =>{
const { jwtSecret } = readKey()
try {
const { exp } = jwt.verify(token, jwtSecret)
if(Date.now() > (exp * 1000)) return { code: -1, msg: 'token expires' }
return { code: 1, msg: 'success' }
} catch (error) {
return { code: -2, msg: error }
}
}
const isProd = () => {
const EXEC_ENV = process.env.EXEC_ENV || 'production'
return EXEC_ENV === 'production'
}
const decrypt = (ciphertext) => {
let { privateKey } = readKey()
const rsakey = new NodeRSA(privateKey)
rsakey.setOptions({ encryptionScheme: 'pkcs1' })
const plaintext = rsakey.decrypt(ciphertext, 'utf8')
return plaintext
}
module.exports = {
readSSHRecord,
writeSSHRecord,
readHostList,
writeHostList,
getLocalNetIP,
throwError,
isIP,
readKey,
writeKey,
randomStr,
verifyToken,
isProd,
decrypt
const fs = require('fs')
const CryptoJS = require('crypto-js')
const rawCrypto = require('crypto')
const NodeRSA = require('node-rsa')
const jwt = require('jsonwebtoken')
const axios = require('axios')
const request = axios.create({ timeout: 3000 })
const { sshRecordPath, hostListPath, keyPath } = require('../config')
const readSSHRecord = () => {
let list
try {
list = JSON.parse(fs.readFileSync(sshRecordPath, 'utf8'))
} catch (error) {
console.log('读取ssh-record错误, 即将重置ssh列表: ', error)
writeSSHRecord([])
}
return list || []
}
const writeSSHRecord = (record = []) => {
fs.writeFileSync(sshRecordPath, JSON.stringify(record, null, 2))
}
const readHostList = () => {
let list
try {
list = JSON.parse(fs.readFileSync(hostListPath, 'utf8'))
} catch (error) {
console.log('读取host-list错误, 即将重置host列表: ', error)
writeHostList([])
}
return list || []
}
const writeHostList = (record = []) => {
fs.writeFileSync(hostListPath, JSON.stringify(record, null, 2))
}
const readKey = () => {
let keyObj = JSON.parse(fs.readFileSync(keyPath, 'utf8'))
return keyObj
}
const writeKey = (keyObj = {}) => {
fs.writeFileSync(keyPath, JSON.stringify(keyObj, null, 2))
}
// 为空时请求本地IP
const getNetIPInfo = async (ip = '') => {
try {
let date = getUTCDate(8)
let ipUrls = [`http://ip-api.com/json/${ ip }?lang=zh-CN`, `http://whois.pconline.com.cn/ipJson.jsp?json=true&ip=${ ip }`]
let result = await Promise.allSettled(ipUrls.map(url => request.get(url)))
let [ipApi, pconline] = result
if(ipApi.status === 'fulfilled') {
let { query: ip, country, regionName, city } = ipApi.value.data
// console.log({ ip, country, city: regionName + city })
return { ip, country, city: regionName + city, date }
}
if(pconline.status === 'fulfilled') {
let { ip, pro, city, addr } = pconline.value.data
// console.log({ ip, country: pro || addr, city })
return { ip, country: pro || addr, city, date }
}
throw Error('获取IP信息API出错,请排查或更新API')
} catch (error) {
console.error('getIpInfo Error: ', error)
return {
ip: '未知',
country: '未知',
city: '未知',
error
}
}
}
const throwError = ({ status = 500, msg = 'defalut error' } = {}) => {
const err = new Error(msg)
err.status = status // 主动抛错
throw err
}
const isIP = (ip = '') => {
const isIPv4 = /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/
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:/
return isIPv4.test(ip) || isIPv6.test(ip)
}
const randomStr = (e) =>{
e = e || 16
let str = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678',
a = str.length,
res = ''
for (let i = 0; i < e; i++) res += str.charAt(Math.floor(Math.random() * a))
return res
}
// 校验token与登录IP
const verifyAuth = (token, clientIp) =>{
token = AESDecrypt(token) // 先aes解密
const { commonKey } = readKey()
try {
const { exp } = jwt.verify(token, commonKey)
// console.log('校验token', new Date(), '---', new Date(exp * 1000))
if(Date.now() > (exp * 1000)) return { code: -1, msg: 'token expires' } // 过期
let lastLoginIp = global.loginRecord[0] ? global.loginRecord[0].ip : ''
console.log('校验客户端IP', clientIp)
console.log('最后登录的IP', lastLoginIp)
// 判断: (生产环境)clientIp与上次登录成功IP不一致
if(isProd() && (!lastLoginIp || !clientIp || !clientIp.includes(lastLoginIp))) {
return { code: -1, msg: '登录IP发生变化, 需重新登录' } // IP与上次登录访问的不一致
}
// console.log('token验证成功')
return { code: 1, msg: 'success' } // 验证成功
} catch (error) {
// 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 os = require('os')
let cpu = osu.cpu
let mem = osu.mem
let drive = osu.drive
let netstat = osu.netstat
let osuOs = osu.os
let users = osu.users
async function cpuInfo() {
let cpuUsage = await cpu.usage(300)
let cpuCount = cpu.count()
let cpuModel = cpu.model()
return {
cpuUsage,
cpuCount,
cpuModel
}
}
async function memInfo() {
let memInfo = await mem.info()
return {
...memInfo
}
}
async function driveInfo() {
let driveInfo = {}
try {
driveInfo = await drive.info()
} catch {
// console.log(driveInfo)
}
return driveInfo
}
async function netstatInfo() {
let netstatInfo = await netstat.inOut(300)
return netstatInfo === 'not supported' ? {} : netstatInfo
}
async function osInfo() {
let type = os.type()
let platform = os.platform()
let release = os.release()
let uptime = osuOs.uptime()
let ip = osuOs.ip()
let hostname = osuOs.hostname()
let arch = osuOs.arch()
return {
type,
platform,
release,
ip,
hostname,
arch,
uptime
}
}
async function openedCount() {
let openedCount = await users.openedCount()
return openedCount === 'not supported' ? 0 : openedCount
}
module.exports = async () => {
let data = {}
try {
data = {
cpuInfo: await cpuInfo(),
memInfo: await memInfo(),
driveInfo: await driveInfo(),
netstatInfo: await netstatInfo(),
osInfo: await osInfo(),
openedCount: await openedCount()
}
return data
} catch(err){
console.error('获取系统信息出错:', err)
return err.toString()
}
}
const osu = require('node-os-utils')
const os = require('os')
let cpu = osu.cpu
let mem = osu.mem
let drive = osu.drive
let netstat = osu.netstat
let osuOs = osu.os
let users = osu.users
async function cpuInfo() {
let cpuUsage = await cpu.usage(200)
let cpuCount = cpu.count()
let cpuModel = cpu.model()
return {
cpuUsage,
cpuCount,
cpuModel
}
}
async function memInfo() {
let memInfo = await mem.info()
return {
...memInfo
}
}
async function driveInfo() {
let driveInfo = {}
try {
driveInfo = await drive.info()
} catch {
// console.log(driveInfo)
}
return driveInfo
}
async function netstatInfo() {
let netstatInfo = await netstat.inOut()
return netstatInfo === 'not supported' ? {} : netstatInfo
}
async function osInfo() {
let type = os.type()
let platform = os.platform()
let release = os.release()
let uptime = osuOs.uptime()
let ip = osuOs.ip()
let hostname = osuOs.hostname()
let arch = osuOs.arch()
return {
type,
platform,
release,
ip,
hostname,
arch,
uptime
}
}
async function openedCount() {
let openedCount = await users.openedCount()
return openedCount === 'not supported' ? 0 : openedCount
}
module.exports = async () => {
let data = {}
try {
data = {
cpuInfo: await cpuInfo(),
memInfo: await memInfo(),
driveInfo: await driveInfo(),
netstatInfo: await netstatInfo(),
osInfo: await osInfo(),
openedCount: await openedCount()
}
return data
} catch(err){
console.error('获取系统信息出错:', err)
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",
"version": "0.0.1",
"description": "easynode-server",
"bin": "./bin/www",
"pkg": {
"outputPath": "dist",
"scripts": "./*",
"assets": "./*"
},
"scripts": {
"local": "cross-env EXEC_ENV=local nodemon ./app/main.js",
"server": "cross-env EXEC_ENV=production nodemon ./app/main.js",
"start": "pm2 start ./app/main.js",
"pkgwin": "pkg . -t node16-win-x64",
"pkglinux": "pkg . -t node16-linux-x64"
},
"keywords": [],
"author": "",
"license": "ISC",
"nodemonConfig": {
"ignore": [
"*.json"
]
},
"dependencies": {
"@koa/cors": "^3.1.0",
"axios": "^0.21.4",
"is-ip": "^4.0.0",
"jsonwebtoken": "^8.5.1",
"koa": "^2.13.1",
"koa-body": "^4.2.0",
"koa-compose": "^4.1.0",
"koa-compress": "^5.1.0",
"koa-jwt": "^4.0.3",
"koa-router": "^10.0.0",
"koa-sslify": "^5.0.0",
"koa-static": "^5.0.0",
"koa2-connect-history-api-fallback": "^0.1.3",
"log4js": "^6.4.4",
"node-os-utils": "^1.3.6",
"node-rsa": "^1.1.1",
"node-schedule": "^2.1.0",
"socket.io": "^4.4.1",
"socket.io-client": "^4.5.1",
"ssh2": "^1.10.0"
},
"devDependencies": {
"cross-env": "^7.0.3",
"eslint": "^7.32.0",
"nodemon": "^2.0.15",
"pkg": "5.6"
}
}
{
"name": "easynode-server",
"version": "1.1.0",
"description": "easynode-server",
"bin": "./bin/www",
"pkg": {
"outputPath": "dist",
"scripts": "./*",
"assets": "./*"
},
"scripts": {
"local": "cross-env EXEC_ENV=local nodemon ./app/main.js",
"server": "cross-env EXEC_ENV=production nodemon ./app/main.js",
"start": "pm2 start ./app/main.js",
"pkgwin": "pkg . -t node16-win-x64",
"pkglinux:x86": "pkg . -t node16-linux-x64",
"pkglinux:arm": "pkg . -t node16-linux-arm64"
},
"keywords": [],
"author": "",
"license": "ISC",
"nodemonConfig": {
"ignore": [
"*.json"
]
},
"dependencies": {
"@koa/cors": "^3.1.0",
"axios": "^0.21.4",
"crypto-js": "^4.1.1",
"global": "^4.4.0",
"is-ip": "^4.0.0",
"jsonwebtoken": "^8.5.1",
"koa": "^2.13.1",
"koa-body": "^4.2.0",
"koa-compose": "^4.1.0",
"koa-compress": "^5.1.0",
"koa-jwt": "^4.0.3",
"koa-router": "^10.0.0",
"koa-sslify": "^5.0.0",
"koa-static": "^5.0.0",
"koa2-connect-history-api-fallback": "^0.1.3",
"log4js": "^6.4.4",
"node-os-utils": "^1.3.6",
"node-rsa": "^1.1.1",
"node-schedule": "^2.1.0",
"socket.io": "^4.4.1",
"socket.io-client": "^4.5.1",
"ssh2": "^1.10.0"
},
"devDependencies": {
"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