test plus

This commit is contained in:
chaos-zhu 2024-11-04 23:34:28 +08:00
parent 1171d6e6cc
commit 1b3b2892d0
16 changed files with 234 additions and 176 deletions

View File

@ -2,4 +2,7 @@
DEBUG=1
# 访问IP限制
allowedIPs=['127.0.0.1']
allowedIPs=['127.0.0.1']
# 激活PLUS功能的授权码
PLUS_KEY=

View File

@ -17,6 +17,7 @@ module.exports = {
notifyConfigDBPath: path.join(process.cwd(),'app/db/notify-config.db'),
onekeyDBPath: path.join(process.cwd(),'app/db/onekey.db'),
logDBPath: path.join(process.cwd(),'app/db/log.db'),
plusDBPath: path.join(process.cwd(),'app/db/plus.db'),
apiPrefix: '/api/v1',
logConfig: {
outDir: path.join(process.cwd(),'./app/logs'),

View File

@ -51,7 +51,7 @@ async function updateHost({ res, request }) {
hosts,
id,
host: newHost, name: newName, index, oldHost, expired, expiredNotify, group, consoleUrl, remark,
port, clientPort, username, authType, password, privateKey, credential, command, tempKey
port, clientPort, username, authType, password, privateKey, credential, command, tempKey, jumpHosts = []
}
} = request
let isBatch = Array.isArray(hosts)
@ -73,7 +73,6 @@ async function updateHost({ res, request }) {
target[authType] = await AESEncryptAsync(clearSSHKey)
// console.log(`${ authType }__commonKey加密存储: `, target[authType])
}
delete target._id
delete target.monitorData
delete target.tempKey
Object.assign(oldRecord, target)
@ -85,7 +84,7 @@ async function updateHost({ res, request }) {
let updateRecord = {
name: newName, host: newHost, index, expired, expiredNotify, group, consoleUrl, remark,
port, clientPort, username, authType, password, privateKey, credential, command
port, clientPort, username, authType, password, privateKey, credential, command, jumpHosts
}
let oldRecord = await hostListDB.findOneAsync({ _id: id })

View File

@ -5,9 +5,10 @@ const QRCode = require('qrcode')
const { sendNoticeAsync } = require('../utils/notify')
const { RSADecryptAsync, AESEncryptAsync, SHA1Encrypt } = require('../utils/encrypt')
const { getNetIPInfo } = require('../utils/tools')
const { KeyDB, LogDB } = require('../utils/db-class')
const { KeyDB, LogDB, PlusDB } = require('../utils/db-class')
const keyDB = new KeyDB().getInstance()
const logDB = new LogDB().getInstance()
const plusDB = new PlusDB().getInstance()
const getpublicKey = async ({ res }) => {
let { publicKey: data } = await keyDB.findOneAsync({})
@ -164,6 +165,11 @@ const disableMFA2 = async ({ res }) => {
res.success({ msg: 'success' })
}
const getPlusInfo = async ({ res }) => {
const data = await plusDB.findOneAsync({})
res.success({ data, msg: 'success' })
}
module.exports = {
login,
getpublicKey,
@ -172,5 +178,6 @@ module.exports = {
getMFA2Status,
getMFA2Code,
enableMFA2,
disableMFA2
disableMFA2,
getPlusInfo
}

View File

@ -0,0 +1,44 @@
const fs = require('fs-extra')
const path = require('path')
const CryptoJS = require('crypto-js')
require('dotenv').config()
console.log(process.env.PLUS_DECRYPT_KEY)
async function encryptPlusClearFiles(dir) {
try {
const files = await fs.readdir(dir)
for (const file of files) {
const fullPath = path.join(dir, file)
const stat = await fs.stat(fullPath)
if (stat.isDirectory()) {
await encryptPlusClearFiles(fullPath)
} else if (file === 'plus-clear.js') {
const content = await fs.readFile(fullPath, 'utf-8')
// global.PLUS_DECRYPT_KEY
const encryptedContent = CryptoJS.AES.encrypt(content, process.env.PLUS_DECRYPT_KEY).toString()
const newPath = path.join(path.dirname(fullPath), 'plus.js')
await fs.writeFile(newPath, encryptedContent)
console.log(`已加密文件: ${fullPath}`)
console.log(`生成加密文件: ${newPath}`)
}
}
} catch (error) {
console.error('加密过程出错:', error)
}
}
const appDir = path.join(__dirname)
console.log(appDir)
// encryptPlusClearFiles(appDir)
// .then(() => {
// console.log('加密完成!')
// })
// .catch(error => {
// console.error('程序执行出错:', error)
// })

View File

@ -1,8 +1,7 @@
const consola = require('consola')
global.consola = consola
const { httpServer } = require('./server')
const initDB = require('./db')
const scheduleJob = require('./schedule')
require(process.env.NODE_ENV === 'dev' ? './utils/plus-clear' : './utils/plus')()
async function main() {
await initDB()

View File

@ -1,6 +1,6 @@
const { getSSHList, addSSH, updateSSH, removeSSH, getCommand } = require('../controller/ssh')
const { getHostList, addHost, updateHost, removeHost, importHost } = require('../controller/host')
const { login, getpublicKey, updatePwd, getEasynodeVersion, getMFA2Status, getMFA2Code, enableMFA2, disableMFA2 } = require('../controller/user')
const { login, getpublicKey, updatePwd, getEasynodeVersion, getMFA2Status, getMFA2Code, enableMFA2, disableMFA2, getPlusInfo } = require('../controller/user')
const { getNotifyConfig, updateNotifyConfig, getNotifyList, updateNotifyList } = require('../controller/notify')
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
const { getScriptList, getLocalScriptList, addScript, updateScriptList, removeScript } = require('../controller/scripts')
@ -101,6 +101,11 @@ const user = [
method: 'post',
path: '/mfa2-disable',
controller: disableMFA2
},
{
method: 'get',
path: '/plus-info',
controller: getPlusInfo
}
]
const notify = [

View File

@ -1,33 +0,0 @@
const schedule = require('node-schedule')
const { sendNoticeAsync } = require('../utils/notify')
const { formatTimestamp } = require('../utils/tools')
const { HostListDB } = require('../utils/db-class')
const hostListDB = new HostListDB().getInstance()
const expiredNotifyJob = async () => {
consola.info('=====开始检测服务器到期时间=====', new Date())
const hostList = await hostListDB.findAsync({})
for (const item of hostList) {
if (!item.expiredNotify) continue
const { host, name, expired, consoleUrl } = item
const restDay = Number(((expired - Date.now()) / (1000 * 60 * 60 * 24)).toFixed(1))
console.log(Date.now(), restDay)
let title = '服务器到期提醒'
let content = `别名: ${ name }\nIP: ${ host }\n到期时间:${ formatTimestamp(expired, 'week') }\n控制台: ${ consoleUrl || '未填写' }`
if (0 <= restDay && restDay <= 1) {
let temp = '有服务器将在一天后到期,请关注\n'
sendNoticeAsync('host_expired', title, temp + content)
} else if (3 <= restDay && restDay < 4) {
let temp = '有服务器将在三天后到期,请关注\n'
sendNoticeAsync('host_expired', title, temp + content)
} else if (7 <= restDay && restDay < 8) {
let temp = '有服务器将在七天后到期,请关注\n'
sendNoticeAsync('host_expired', title, temp + content)
}
}
}
module.exports = () => {
// 每天中午12点执行一次。
schedule.scheduleJob('0 0 12 1/1 * ?', expiredNotifyJob)
}

View File

@ -1,5 +1,32 @@
const expiredNotify = require('./expired-notify')
const schedule = require('node-schedule')
const { sendNoticeAsync } = require('../utils/notify')
const { formatTimestamp } = require('../utils/tools')
const { HostListDB } = require('../utils/db-class')
const hostListDB = new HostListDB().getInstance()
const expiredNotifyJob = async () => {
consola.info('=====开始检测服务器到期时间=====', new Date())
const hostList = await hostListDB.findAsync({})
for (const item of hostList) {
if (!item.expiredNotify) continue
const { host, name, expired, consoleUrl } = item
const restDay = Number(((expired - Date.now()) / (1000 * 60 * 60 * 24)).toFixed(1))
console.log(Date.now(), restDay)
let title = '服务器到期提醒'
let content = `别名: ${ name }\nIP: ${ host }\n到期时间:${ formatTimestamp(expired, 'week') }\n控制台: ${ consoleUrl || '未填写' }`
if (0 <= restDay && restDay <= 1) {
let temp = '有服务器将在一天后到期,请关注\n'
sendNoticeAsync('host_expired', title, temp + content)
} else if (3 <= restDay && restDay < 4) {
let temp = '有服务器将在三天后到期,请关注\n'
sendNoticeAsync('host_expired', title, temp + content)
} else if (7 <= restDay && restDay < 8) {
let temp = '有服务器将在七天后到期,请关注\n'
sendNoticeAsync('host_expired', title, temp + content)
}
}
}
module.exports = () => {
expiredNotify()
schedule.scheduleJob('0 0 12 1/1 * ?', expiredNotifyJob)
}

View File

View File

@ -1,16 +1,15 @@
const { Server } = require('socket.io')
const { Client: SSHClient } = require('ssh2')
const { verifyAuthSync } = require('../utils/verify-auth')
const { AESDecryptAsync } = require('../utils/encrypt')
const { sendNoticeAsync } = require('../utils/notify')
const { isAllowedIp, ping } = require('../utils/tools')
const { HostListDB, CredentialsDB } = require('../utils/db-class')
const { HostListDB } = require('../utils/db-class')
const { getConnectionOptions, connectByJumpHosts } = require(process.env.NODE_ENV === 'dev' ? './plus-clear' : './plus')
const hostListDB = new HostListDB().getInstance()
const credentialsDB = new CredentialsDB().getInstance()
function createInteractiveShell(socket, sshClient) {
function createInteractiveShell(socket, targetSSHClient) {
return new Promise((resolve) => {
sshClient.shell({ term: 'xterm-color' }, (err, stream) => {
targetSSHClient.shell({ term: 'xterm-color' }, (err, stream) => {
resolve(stream)
if (err) return socket.emit('output', err.toString())
// 终端输出
@ -20,70 +19,38 @@ function createInteractiveShell(socket, sshClient) {
})
.on('close', () => {
consola.info('交互终端已关闭')
sshClient.end()
targetSSHClient.end()
})
socket.emit('connect_shell_success') // 已连接终端web端可以执行指令了
})
})
}
// function execShell(sshClient, command = '', callback) {
// if (!command) return
// let result = ''
// sshClient.exec(`source ~/.bashrc && ${ command }`, (err, stream) => {
// if (err) return callback(err.toString())
// stream
// .on('data', (data) => {
// result += data.toString()
// })
// .stderr
// .on('data', (data) => {
// result += data.toString()
// })
// .on('close', () => {
// consola.info('一次性指令执行完成:', command)
// callback(result)
// })
// .on('error', (error) => {
// console.log('Error:', error.toString())
// })
// })
// }
async function createTerminal(hostId, socket, sshClient) {
// eslint-disable-next-line no-async-promise-executor
async function createTerminal(hostId, socket, targetSSHClient) {
return new Promise(async (resolve) => {
const hostList = await hostListDB.findAsync({})
const targetHostInfo = hostList.find(item => item._id === hostId) || {}
let { authType, host, port, username, name } = targetHostInfo
if (!host) return socket.emit('create_fail', `查找hostId【${ hostId }】凭证信息失败`)
let authInfo = { host, port, username }
// 统一使用commonKey解密
const targetHostInfo = await hostListDB.findOneAsync({ _id: hostId })
if (!targetHostInfo) return socket.emit('create_fail', `查找hostId【${ hostId }】凭证信息失败`)
let { authType, host, port, username, name, jumpHosts } = targetHostInfo
try {
// 解密放到try里面防止报错【commonKey必须配对, 否则需要重新添加服务器密钥】
if (authType === 'credential') {
let credentialId = await AESDecryptAsync(targetHostInfo[authType])
const sshRecord = await credentialsDB.findOneAsync({ _id: credentialId })
authInfo.authType = sshRecord.authType
authInfo[authInfo.authType] = await AESDecryptAsync(sshRecord[authInfo.authType])
} else {
authInfo[authType] = await AESDecryptAsync(targetHostInfo[authType])
let { authInfo: targetConnectionOptions } = await getConnectionOptions(hostId)
let jumpHostResult = await connectByJumpHosts(jumpHosts, targetConnectionOptions.host, targetConnectionOptions.port, socket)
if (jumpHostResult) {
targetConnectionOptions.sock = jumpHostResult.sock
}
consola.info('准备连接终端:', host)
// targetHostInfo[targetHostInfo.authType] = await AESDecryptAsync(targetHostInfo[targetHostInfo.authType])
socket.emit('terminal_print_info', `准备连接目标终端: ${ name } - ${ host }`)
socket.emit('terminal_print_info', `连接信息: ssh ${ username }@${ host } -p ${ port } -> ${ authType }`)
consola.info('准备连接目标终端:', host)
consola.log('连接信息', { username, port, authType })
sshClient
targetSSHClient
.on('ready', async() => {
sendNoticeAsync('host_login', '终端登录', `别名: ${ name } \n IP${ host } \n 端口:${ port } \n 状态: 登录成功`)
socket.emit('terminal_print_info', `终端连接成功: ${ name } - ${ host }`)
consola.success('终端连接成功:', host)
socket.emit('connect_terminal_success', `终端连接成功:${ host }`)
let stream = await createInteractiveShell(socket, sshClient)
let stream = await createInteractiveShell(socket, targetSSHClient)
resolve(stream)
// execShell(sshClient, 'history', (data) => {
// data = data.split('\n').filter(item => item)
// console.log(data)
// socket.emit('terminal_command_history', data)
// })
})
.on('close', () => {
consola.info('终端连接断开close: ', host)
@ -96,7 +63,7 @@ async function createTerminal(hostId, socket, sshClient) {
socket.emit('connect_fail', err.message)
})
.connect({
...authInfo
...targetConnectionOptions
// debug: (info) => console.log(info)
})
@ -123,7 +90,7 @@ module.exports = (httpServer) => {
return
}
consola.success('terminal websocket 已连接')
let sshClient = null
let targetSSHClient = null
socket.on('create', async ({ hostId, token }) => {
const { code } = await verifyAuthSync(token, requestIP)
if (code !== 1) {
@ -131,16 +98,10 @@ module.exports = (httpServer) => {
socket.disconnect()
return
}
sshClient = new SSHClient()
// 尝试手动断开调试再次连接后终端输出内容为4份相同的输出导致异常
// setTimeout(() => {
// sshClient.end()
// }, 3000)
targetSSHClient = new SSHClient()
let stream = null
function listenerInput(key) {
if (sshClient._sock.writable === false) return consola.info('终端连接已关闭,禁止输入')
if (targetSSHClient._sock.writable === false) return consola.info('终端连接已关闭,禁止输入')
stream && stream.write(key)
}
function resizeShell({ rows, cols }) {
@ -155,20 +116,20 @@ module.exports = (httpServer) => {
consola.info('重连终端: ', hostId)
socket.off('input', listenerInput) // 取消监听,重新注册监听,操作新的stream
socket.off('resize', resizeShell)
sshClient?.end()
sshClient?.destroy()
sshClient = null
targetSSHClient?.end()
targetSSHClient?.destroy()
targetSSHClient = null
stream = null
setTimeout(async () => {
// 初始化新的SSH客户端对象
sshClient = new SSHClient()
stream = await createTerminal(hostId, socket, sshClient)
targetSSHClient = new SSHClient()
stream = await createTerminal(hostId, socket, targetSSHClient)
socket.emit('reconnect_terminal_success')
socket.on('input', listenerInput)
socket.on('resize', resizeShell)
}, 3000)
})
stream = await createTerminal(hostId, socket, sshClient)
stream = await createTerminal(hostId, socket, targetSSHClient)
})
socket.on('get_ping',async (ip) => {

View File

@ -8,7 +8,8 @@ const {
groupConfDBPath,
scriptsDBPath,
onekeyDBPath,
logDBPath
logDBPath,
plusDBPath
} = require('../config')
module.exports.KeyDB = class KeyDB {
@ -117,4 +118,15 @@ module.exports.LogDB = class LogDB {
getInstance() {
return LogDB.instance
}
}
module.exports.PlusDB = class PlusDB {
constructor() {
if (!PlusDB.instance) {
PlusDB.instance = new Datastore({ filename: plusDBPath, autoload: true })
}
}
getInstance() {
return PlusDB.instance
}
}

0
server/app/utils/plus.js Normal file
View File

View File

@ -89,6 +89,35 @@ const getNetIPInfo = async (searchIp = '') => {
}
}
const getLocalNetIP = async () => {
try {
let ipUrls = [
'http://whois.pconline.com.cn/ipJson.jsp?json=true',
'https://www.ip.cn/api/index?ip=&type=0',
'https://freeipapi.com/api/json'
]
let result = await Promise.allSettled(ipUrls.map(url => axios.get(url)))
let [pconline, ipCN, freeipapi] = result
if (pconline.status === 'fulfilled') {
let ip = pconline.value?.data?.ip
if (ip) return ip
}
if (ipCN.status === 'fulfilled') {
let ip = ipCN.value?.data?.ip
consola.log('ipCN:', ip)
if (ip) return ip
}
if (freeipapi.status === 'fulfilled') {
let ip = pconline.value?.data?.ipAddress
if (ip) return ip
}
return null
} catch (error) {
console.error('getIpInfo Error: ', error?.message || error)
return null
}
}
function isLocalIP(ip) {
// Check if IPv4 or IPv6 address
const isIPv4 = net.isIPv4(ip)
@ -159,7 +188,7 @@ const isIP = (ip = '') => {
return isIPv4.test(ip) || isIPv6.test(ip)
}
const randomStr = (len) =>{
const randomStr = (len) => {
len = len || 16
let str = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678',
a = str.length,
@ -178,7 +207,7 @@ const getUTCDate = (num = 8) => {
}
const formatTimestamp = (timestamp = Date.now(), format = 'time') => {
if (typeof(timestamp) !== 'number') return '--'
if (typeof (timestamp) !== 'number') return '--'
let date = new Date(timestamp)
let padZero = (num) => String(num).padStart(2, '0')
let year = date.getFullYear()
@ -187,7 +216,7 @@ const formatTimestamp = (timestamp = Date.now(), format = 'time') => {
let hours = padZero(date.getHours())
let minute = padZero(date.getMinutes())
let second = padZero(date.getSeconds())
let weekday = ['周日', '周一', '周二', '周三', '周四', '周五', '周六' ]
let weekday = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
let week = weekday[date.getDay()]
switch (format) {
case 'date':
@ -284,6 +313,7 @@ const ping = (ip, timeout = 5000) => {
module.exports = {
getNetIPInfo,
getLocalNetIP,
throwError,
isIP,
randomStr,

View File

@ -1,2 +1,4 @@
const consola = require('consola')
global.consola = consola
require('dotenv').config()
require('./app/main.js')

View File

@ -1,58 +1,59 @@
{
"name": "server",
"version": "1.0.0",
"description": "easynode-server",
"bin": "./bin/www",
"scripts": {
"local": "cross-env EXEC_ENV=local nodemon index.js",
"prod": "cross-env EXEC_ENV=production nodemon index.js",
"start": "node ./index.js",
"lint": "eslint . --ext .js,.vue",
"lint:fix": "eslint . --ext .js,.jsx,.cjs,.mjs --fix"
},
"keywords": [],
"author": "",
"license": "ISC",
"nodemonConfig": {
"ignore": [
"*.json"
]
},
"dependencies": {
"@koa/cors": "^5.0.0",
"@seald-io/nedb": "^4.0.4",
"axios": "^1.7.4",
"consola": "^3.2.3",
"cross-env": "^7.0.3",
"crypto-js": "^4.2.0",
"dotenv": "^16.4.5",
"fs-extra": "^11.2.0",
"global": "^4.4.0",
"iconv-lite": "^0.6.3",
"jsonwebtoken": "^9.0.2",
"koa": "^2.15.3",
"koa-body": "^6.0.1",
"koa-compose": "^4.1.0",
"koa-compress": "^5.1.1",
"koa-jwt": "^4.0.4",
"koa-router": "^12.0.1",
"koa-sslify": "^5.0.1",
"koa-static": "^5.0.0",
"koa2-connect-history-api-fallback": "^0.1.3",
"log4js": "^6.9.1",
"node-os-utils": "^1.3.7",
"node-rsa": "^1.1.1",
"node-schedule": "^2.1.1",
"nodemailer": "^6.9.14",
"qrcode": "^1.5.4",
"socket.io": "^4.7.5",
"socket.io-client": "^4.7.5",
"speakeasy": "^2.0.0",
"ssh2": "^1.15.0",
"ssh2-sftp-client": "^10.0.3"
},
"devDependencies": {
"eslint": "^8.56.0",
"nodemon": "^3.1.4"
}
}
{
"name": "server",
"version": "3.0.0",
"description": "easynode-server",
"bin": "./bin/www",
"scripts": {
"local": "cross-env EXEC_ENV=local nodemon index.js",
"prod": "cross-env EXEC_ENV=production nodemon index.js",
"start": "node ./index.js",
"lint": "eslint . --ext .js,.vue",
"lint:fix": "eslint . --ext .js,.jsx,.cjs,.mjs --fix",
"encrypt": "node ./app/encrypt-file.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"nodemonConfig": {
"ignore": [
"*.json"
]
},
"dependencies": {
"@koa/cors": "^5.0.0",
"@seald-io/nedb": "^4.0.4",
"axios": "^1.7.4",
"consola": "^3.2.3",
"cross-env": "^7.0.3",
"crypto-js": "^4.2.0",
"dotenv": "^16.4.5",
"fs-extra": "^11.2.0",
"global": "^4.4.0",
"iconv-lite": "^0.6.3",
"jsonwebtoken": "^9.0.2",
"koa": "^2.15.3",
"koa-body": "^6.0.1",
"koa-compose": "^4.1.0",
"koa-compress": "^5.1.1",
"koa-jwt": "^4.0.4",
"koa-router": "^12.0.1",
"koa-sslify": "^5.0.1",
"koa-static": "^5.0.0",
"koa2-connect-history-api-fallback": "^0.1.3",
"log4js": "^6.9.1",
"node-os-utils": "^1.3.7",
"node-rsa": "^1.1.1",
"node-schedule": "^2.1.1",
"nodemailer": "^6.9.14",
"qrcode": "^1.5.4",
"socket.io": "^4.7.5",
"socket.io-client": "^4.7.5",
"speakeasy": "^2.0.0",
"ssh2": "^1.15.0",
"ssh2-sftp-client": "^10.0.3"
},
"devDependencies": {
"eslint": "^8.56.0",
"nodemon": "^3.1.4"
}
}