From c55d3bddd682ef4f1fed8c988ff76c73ed9411c3 Mon Sep 17 00:00:00 2001 From: chaos-zhu Date: Mon, 4 Nov 2024 23:34:37 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20plus&=E5=8A=9F=E8=83=BD=E9=87=8D?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + README.md | 3 + package.json | 3 +- {server/app => script}/encrypt-file.js | 31 +- script/update-version.js | 21 ++ server/app/controller/user.js | 4 +- server/app/main.js | 3 +- server/app/middlewares/log4.js | 24 +- server/app/socket/plus.js | 1 + server/app/socket/terminal.js | 89 ++--- server/app/utils/decrypt-file.js | 53 +++ server/app/utils/get-plus.js | 93 ++++++ server/app/utils/plus.js | 0 server/package.json | 3 +- web/package.json | 2 +- web/src/api/index.js | 6 +- web/src/app.vue | 2 +- web/src/assets/plus.png | Bin 0 -> 10127 bytes web/src/components/common/PlusSupportTip.vue | 42 +++ web/src/components/menuList.vue | 2 +- web/src/components/top-bar.vue | 305 ++++++++++++++++-- web/src/store/index.js | 20 +- web/src/utils/enum.js | 2 - web/src/views/login/index.vue | 2 +- web/src/views/server/components/host-form.vue | 56 +++- .../views/terminal/components/info-side.vue | 6 +- .../terminal/components/terminal-setting.vue | 27 +- .../terminal/components/terminal-tab.vue | 147 +++++---- .../views/terminal/components/terminal.vue | 4 +- web/src/views/terminal/index.vue | 5 +- 30 files changed, 782 insertions(+), 176 deletions(-) rename {server/app => script}/encrypt-file.js (61%) create mode 100644 script/update-version.js create mode 100644 server/app/utils/decrypt-file.js create mode 100644 server/app/utils/get-plus.js delete mode 100644 server/app/utils/plus.js create mode 100644 web/src/assets/plus.png create mode 100644 web/src/components/common/PlusSupportTip.vue diff --git a/.gitignore b/.gitignore index 4790d71..ae8fb72 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ server/app/db/* plan.md .env .env.local +.env-encrypt-key +*clear.js diff --git a/README.md b/README.md index 86307f2..cbc166e 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ docker run -d -p 8082:8082 --name=easynode --restart=always -v /root/easynode/db:/easynode/app/db chaoszhu/easynode ``` 环境变量: +- `PLUS_KEY`: 激活PLUS功能的授权码 - `DEBUG`: 启动debug日志 0:关闭 1:开启, 默认关闭 - `ALLOWED_IPS`: 可以访问服务的IP白名单, 多个使用逗号分隔, 支持填写部分ip前缀, 例如: `-e ALLOWED_IPS=127.0.0.1,196.168` @@ -119,3 +120,5 @@ webssh与监控服务都将以`该服务器作为中转`。中国大陆用户建 ## License [MIT](LICENSE). Copyright (c). + +![访问数](https://profile-counter.glitch.me/easynode/count.svg) diff --git a/package.json b/package.json index 06ad815..4dd7128 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "license": "ISC", "scripts": { "dev": "concurrently \"yarn workspace web run dev\" \"yarn workspace server run local\"", - "clean": "rimraf web/node_modules server/node_modules client/node_modules node_modules" + "clean": "rimraf web/node_modules server/node_modules client/node_modules node_modules", + "encrypt": "node ./script/encrypt-file.js" }, "bugs": { "url": "https://github.com/chaos-zhu/easynode/issues" diff --git a/server/app/encrypt-file.js b/script/encrypt-file.js similarity index 61% rename from server/app/encrypt-file.js rename to script/encrypt-file.js index dec211e..01061c7 100644 --- a/server/app/encrypt-file.js +++ b/script/encrypt-file.js @@ -1,11 +1,17 @@ const fs = require('fs-extra') const path = require('path') const CryptoJS = require('crypto-js') -require('dotenv').config() -console.log(process.env.PLUS_DECRYPT_KEY) +require('dotenv').config({ path: '.env-encrypt-key' }) +const version = require('../server/package.json').version + +console.log('加密版本:', version, '加密密钥:', process.env.PLUS_DECRYPT_KEY) async function encryptPlusClearFiles(dir) { try { + if (dir.includes('node_modules')) { + return; + } + const files = await fs.readdir(dir) for (const file of files) { @@ -17,7 +23,6 @@ async function encryptPlusClearFiles(dir) { } 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') @@ -25,7 +30,7 @@ async function encryptPlusClearFiles(dir) { await fs.writeFile(newPath, encryptedContent) console.log(`已加密文件: ${fullPath}`) - console.log(`生成加密文件: ${newPath}`) + console.log(`生成加密文件: ${newPath} `) } } } catch (error) { @@ -33,12 +38,12 @@ async function encryptPlusClearFiles(dir) { } } -const appDir = path.join(__dirname) -console.log(appDir) -// encryptPlusClearFiles(appDir) -// .then(() => { -// console.log('加密完成!') -// }) -// .catch(error => { -// console.error('程序执行出错:', error) -// }) \ No newline at end of file +const appDir = path.join(__dirname, '../server') + +encryptPlusClearFiles(appDir) + .then(() => { + console.log(`${version} 版本加密完成!`) + }) + .catch(error => { + console.error('程序执行出错:', error) + }) \ No newline at end of file diff --git a/script/update-version.js b/script/update-version.js new file mode 100644 index 0000000..26fce88 --- /dev/null +++ b/script/update-version.js @@ -0,0 +1,21 @@ +const fs = require('fs'); +const path = require('path'); + +const newVersion = process.argv[2]; + +if (!newVersion) { + console.error('请提供新版本号,例如: node update-version.js 3.1.0'); + process.exit(1); +} + +const files = [ + path.join(__dirname, 'server/package.json'), + path.join(__dirname, 'web/package.json') +]; + +files.forEach(file => { + const content = JSON.parse(fs.readFileSync(file, 'utf8')); + content.version = newVersion; + fs.writeFileSync(file, JSON.stringify(content, null, 2) + '\n'); + console.log(`已更新 ${file} 的版本到 ${newVersion}`); +}); \ No newline at end of file diff --git a/server/app/controller/user.js b/server/app/controller/user.js index fa299e2..6a673e4 100644 --- a/server/app/controller/user.js +++ b/server/app/controller/user.js @@ -166,7 +166,9 @@ const disableMFA2 = async ({ res }) => { } const getPlusInfo = async ({ res }) => { - const data = await plusDB.findOneAsync({}) + let data = await plusDB.findOneAsync({}) + delete data?._id + delete data?.decryptKey res.success({ data, msg: 'success' }) } diff --git a/server/app/main.js b/server/app/main.js index f8b691f..fc1dc6c 100644 --- a/server/app/main.js +++ b/server/app/main.js @@ -1,12 +1,13 @@ const { httpServer } = require('./server') const initDB = require('./db') const scheduleJob = require('./schedule') -require(process.env.NODE_ENV === 'dev' ? './utils/plus-clear' : './utils/plus')() +const getLicenseInfo = require('./utils/get-plus') async function main() { await initDB() httpServer() scheduleJob() + getLicenseInfo() } main() diff --git a/server/app/middlewares/log4.js b/server/app/middlewares/log4.js index 282b7b1..aa3c320 100644 --- a/server/app/middlewares/log4.js +++ b/server/app/middlewares/log4.js @@ -3,27 +3,28 @@ const { outDir, recordLog } = require('../config').logConfig log4js.configure({ appenders: { - // 控制台输出 - out: { + console: { type: 'stdout', layout: { - type: 'colored' + type: 'pattern', + pattern: '%[%d{yyyy-MM-dd hh:mm:ss.SSS} [%p] -%] %m' } }, - // 保存日志文件 cheese: { type: 'file', - maxLogSize: 512*1024, // unit: bytes 1KB = 1024bytes - filename: `${ outDir }/receive.log` + maxLogSize: 10 * 1024 * 1024, // unit: bytes 1KB = 1024bytes + filename: `${ outDir }/receive.log`, + backups: 10, + compress: true, + keepFileExt: true } }, categories: { default: { - appenders: [ 'out', 'cheese' ], // 配置 - level: 'info' // 只输出info以上级别的日志 + appenders: ['console', 'cheese'], + level: 'debug' } } - // pm2: true }) const logger = log4js.getLogger() @@ -55,4 +56,7 @@ const useLog = () => { } } -module.exports = useLog() \ No newline at end of file +module.exports = useLog() + +// 可以先测试一下日志是否正常工作 +logger.info('日志系统启动') \ No newline at end of file diff --git a/server/app/socket/plus.js b/server/app/socket/plus.js index e69de29..6ad4934 100644 --- a/server/app/socket/plus.js +++ b/server/app/socket/plus.js @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/server/app/socket/terminal.js b/server/app/socket/terminal.js index 6fef38e..60c87a0 100644 --- a/server/app/socket/terminal.js +++ b/server/app/socket/terminal.js @@ -1,18 +1,40 @@ +const path = require('path') const { Server } = require('socket.io') const { Client: SSHClient } = require('ssh2') const { verifyAuthSync } = require('../utils/verify-auth') const { sendNoticeAsync } = require('../utils/notify') const { isAllowedIp, ping } = require('../utils/tools') -const { HostListDB } = require('../utils/db-class') -const { getConnectionOptions, connectByJumpHosts } = require(process.env.NODE_ENV === 'dev' ? './plus-clear' : './plus') +const { AESDecryptAsync } = require('../utils/encrypt') +const { HostListDB, CredentialsDB } = require('../utils/db-class') +const decryptAndExecuteAsync = require('../utils/decrypt-file') const hostListDB = new HostListDB().getInstance() +const credentialsDB = new CredentialsDB().getInstance() + +async function getConnectionOptions(hostId) { + const hostInfo = await hostListDB.findOneAsync({ _id: hostId }) + if (!hostInfo) throw new Error(`Host with ID ${ hostId } not found`) + let { authType, host, port, username, name } = hostInfo + let authInfo = { host, port, username } + try { + if (authType === 'credential') { + let credentialId = await AESDecryptAsync(hostInfo[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(hostInfo[authType]) + } + return { authInfo, name } + } catch (err) { + throw new Error(`解密认证信息失败: ${ err.message }`) + } +} function createInteractiveShell(socket, targetSSHClient) { return new Promise((resolve) => { targetSSHClient.shell({ term: 'xterm-color' }, (err, stream) => { resolve(stream) if (err) return socket.emit('output', err.toString()) - // 终端输出 stream .on('data', (data) => { socket.emit('output', data.toString()) @@ -30,10 +52,15 @@ async function createTerminal(hostId, socket, targetSSHClient) { return new Promise(async (resolve) => { const targetHostInfo = await hostListDB.findOneAsync({ _id: hostId }) if (!targetHostInfo) return socket.emit('create_fail', `查找hostId【${ hostId }】凭证信息失败`) + let connectByJumpHosts = null + let data = await decryptAndExecuteAsync(path.join(__dirname, 'plus.js')) + if (data) { + connectByJumpHosts = data.connectByJumpHosts + } let { authType, host, port, username, name, jumpHosts } = targetHostInfo try { let { authInfo: targetConnectionOptions } = await getConnectionOptions(hostId) - let jumpHostResult = await connectByJumpHosts(jumpHosts, targetConnectionOptions.host, targetConnectionOptions.port, socket) + let jumpHostResult = connectByJumpHosts && await connectByJumpHosts(jumpHosts, targetConnectionOptions.host, targetConnectionOptions.port, socket) if (jumpHostResult) { targetConnectionOptions.sock = jumpHostResult.sock } @@ -43,8 +70,9 @@ async function createTerminal(hostId, socket, targetSSHClient) { consola.info('准备连接目标终端:', host) consola.log('连接信息', { username, port, authType }) + let closeNoticeFlag = false // 避免重复发送通知 targetSSHClient - .on('ready', async() => { + .on('ready', async () => { sendNoticeAsync('host_login', '终端登录', `别名: ${ name } \n IP:${ host } \n 端口:${ port } \n 状态: 登录成功`) socket.emit('terminal_print_info', `终端连接成功: ${ name } - ${ host }`) consola.success('终端连接成功:', host) @@ -52,24 +80,26 @@ async function createTerminal(hostId, socket, targetSSHClient) { let stream = await createInteractiveShell(socket, targetSSHClient) resolve(stream) }) - .on('close', () => { - consola.info('终端连接断开close: ', host) - socket.emit('connect_close') + .on('close', (err) => { + if (closeNoticeFlag) return closeNoticeFlag = false + const closeReason = err ? '发生错误导致连接断开' : '正常断开连接' + consola.info(`终端连接断开(${ closeReason }): ${ host }`) + socket.emit('connect_close', { reason: closeReason }) }) .on('error', (err) => { - consola.log(err) + closeNoticeFlag = true sendNoticeAsync('host_login', '终端登录', `别名: ${ name } \n IP:${ host } \n 端口:${ port } \n 状态: 登录失败`) consola.error('连接终端失败:', host, err.message) - socket.emit('connect_fail', err.message) + socket.emit('connect_terminal_fail', err.message) }) .connect({ ...targetConnectionOptions - // debug: (info) => console.log(info) + // debug: (info) => console.log(info) }) } catch (err) { consola.error('创建终端失败: ', host, err.message) - socket.emit('create_fail', err.message) + socket.emit('create_terminal_fail', err.message) } }) } @@ -78,11 +108,15 @@ module.exports = (httpServer) => { const serverIo = new Server(httpServer, { path: '/terminal', cors: { - origin: '*' // 'http://localhost:8080' + origin: '*' } }) + + let connectionCount = 0 + serverIo.on('connection', (socket) => { - // 前者兼容nginx反代, 后者兼容nodejs自身服务 + connectionCount++ + consola.success(`terminal websocket 已连接 - 当前连接数: ${ connectionCount }`) let requestIP = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address if (!isAllowedIp(requestIP)) { socket.emit('ip_forbidden', 'IP地址不在白名单中') @@ -105,34 +139,14 @@ module.exports = (httpServer) => { stream && stream.write(key) } function resizeShell({ rows, cols }) { - // consola.info('更改tty终端行&列: ', { rows, cols }) stream && stream.setWindow(rows, cols) } socket.on('input', listenerInput) socket.on('resize', resizeShell) - - // 重连 - socket.on('reconnect_terminal', async () => { - consola.info('重连终端: ', hostId) - socket.off('input', listenerInput) // 取消监听,重新注册监听,操作新的stream - socket.off('resize', resizeShell) - targetSSHClient?.end() - targetSSHClient?.destroy() - targetSSHClient = null - stream = null - setTimeout(async () => { - // 初始化新的SSH客户端对象 - 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, targetSSHClient) }) - socket.on('get_ping',async (ip) => { + socket.on('get_ping', async (ip) => { try { socket.emit('ping_data', await ping(ip, 2500)) } catch (error) { @@ -141,7 +155,10 @@ module.exports = (httpServer) => { }) socket.on('disconnect', (reason) => { - consola.info('终端socket连接断开:', reason) + connectionCount-- + consola.info(`终端socket连接断开: ${ reason } - 当前连接数: ${ connectionCount }`) }) }) } + +module.exports.getConnectionOptions = getConnectionOptions diff --git a/server/app/utils/decrypt-file.js b/server/app/utils/decrypt-file.js new file mode 100644 index 0000000..cae3094 --- /dev/null +++ b/server/app/utils/decrypt-file.js @@ -0,0 +1,53 @@ +const fs = require('fs-extra') +const path = require('path') +const CryptoJS = require('crypto-js') +const { AESDecryptAsync } = require('./encrypt') +const { PlusDB } = require('./db-class') +const plusDB = new PlusDB().getInstance() + +function decryptAndExecuteAsync(plusPath) { + return new Promise(async (resolve) => { + try { + let { decryptKey } = await plusDB.findOneAsync({}) + if (!decryptKey) { + throw new Error('缺少解密密钥') + } + decryptKey = await AESDecryptAsync(decryptKey) + const encryptedContent = fs.readFileSync(plusPath, 'utf-8') + const bytes = CryptoJS.AES.decrypt(encryptedContent, decryptKey) + const decryptedContent = bytes.toString(CryptoJS.enc.Utf8) + if (!decryptedContent) { + throw new Error('解密失败,请检查密钥是否正确') + } + const customRequire = (modulePath) => { + if (modulePath.startsWith('.')) { + const absolutePath = path.resolve(path.dirname(plusPath), modulePath) + return require(absolutePath) + } + return require(modulePath) + } + const module = { + exports: {}, + require: customRequire, + __filename: plusPath, + __dirname: path.dirname(plusPath) + } + const wrapper = Function('module', 'exports', 'require', '__filename', '__dirname', + decryptedContent + '\n return module.exports;' + ) + const exports = wrapper( + module, + module.exports, + customRequire, + module.__filename, + module.__dirname + ) + resolve(exports) + } catch (error) { + consola.info('解锁plus功能失败: ', error.message) + resolve(null) + } + }) +} + +module.exports = decryptAndExecuteAsync diff --git a/server/app/utils/get-plus.js b/server/app/utils/get-plus.js new file mode 100644 index 0000000..70d6093 --- /dev/null +++ b/server/app/utils/get-plus.js @@ -0,0 +1,93 @@ +const schedule = require('node-schedule') +const { getLocalNetIP } = require('./tools') +const { AESEncryptAsync } = require('./encrypt') +const version = require('../../package.json').version + +async function getLicenseInfo() { + let key = process.env.PLUS_KEY + if (!key || typeof key !== 'string' || key.length < 20) return + let ip = '' + if (global.serverIp && (Date.now() - global.getServerIpLastTime) / 1000 / 60 < 60) { + ip = global.serverIp + consola.log('get server ip by cache: ', ip) + } else { + ip = await getLocalNetIP() + global.serverIp = ip + global.getServerIpLastTime = Date.now() + consola.log('get server ip by net: ', ip) + } + if (!ip) { + consola.error('activate plus failed: get public ip failed') + global.serverIp = '' + return + } + try { + let response + let method = 'POST' + let body = JSON.stringify({ ip, key, version }) + let headers = { 'Content-Type': 'application/json' } + let timeout = 10000 + try { + response = await fetch('https://en1.221022.xyz/api/licenses/activate', { + method, + headers, + body, + timeout + }) + + if (!response.ok && (response.status !== 403)) { + throw new Error('port1 error') + } + + } catch (error) { + consola.log('retry to activate plus by backup server') + response = await fetch('https://en2.221022.xyz/api/licenses/activate', { + method, + headers, + body, + timeout + }) + } + + if (!response.ok) { + consola.log('activate plus failed: ', response.status) + if (response.status === 403) { + const errMsg = await response.json() + throw { errMsg, clear: true } + } + throw Error({ errMsg: `HTTP error! status: ${ response.status }` }) + } + + const { success, data } = await response.json() + if (success) { + let { decryptKey, expiryDate, usedIPCount, maxIPs, usedIPs } = data + decryptKey = await AESEncryptAsync(decryptKey) + consola.success('activate plus success') + const { PlusDB } = require('./db-class') + const plusData = { key, decryptKey, expiryDate, usedIPCount, maxIPs, usedIPs } + const plusDB = new PlusDB().getInstance() + let count = await plusDB.countAsync({}) + if (count === 0) { + await plusDB.insertAsync(plusData) + } else { + await plusDB.removeAsync({}, { multi: true }) + await plusDB.insertAsync(plusData) + } + } + } catch (error) { + consola.error(`activate plus failed: ${ error.message || error.errMsg?.message }`) + if (error.clear) { + const { PlusDB } = require('./db-class') + const plusDB = new PlusDB().getInstance() + await plusDB.removeAsync({}, { multi: true }) + } + } +} + +const randomHour = Math.floor(Math.random() * 24) +const randomMinute = Math.floor(Math.random() * 60) +const randomDay = Math.floor(Math.random() * 7) +const cronExpression = `${ randomMinute } ${ randomHour } * * ${ randomDay }` +schedule.scheduleJob(cronExpression, getLicenseInfo) + +module.exports = getLicenseInfo diff --git a/server/app/utils/plus.js b/server/app/utils/plus.js deleted file mode 100644 index e69de29..0000000 diff --git a/server/package.json b/server/package.json index 02df313..02a4726 100644 --- a/server/package.json +++ b/server/package.json @@ -8,8 +8,7 @@ "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" + "lint:fix": "eslint . --ext .js,.jsx,.cjs,.mjs --fix" }, "keywords": [], "author": "", diff --git a/web/package.json b/web/package.json index 6993074..fa33191 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "web", - "version": "2.3.0", + "version": "3.0.0", "description": "easynode-web", "private": true, "scripts": { diff --git a/web/src/api/index.js b/web/src/api/index.js index 6cbaa5d..a135ca9 100644 --- a/web/src/api/index.js +++ b/web/src/api/index.js @@ -19,9 +19,9 @@ export default { removeSSH(id) { return axios({ url: `/remove-ssh/${ id }`, method: 'delete' }) }, - // existSSH(host) { - // return axios({ url: '/exist-ssh', method: 'post', data: { host } }) - // }, + getPlusInfo() { + return axios({ url: '/plus-info', method: 'get' }) + }, getCommand(hostId) { return axios({ url: '/command', method: 'get', params: { hostId } }) }, diff --git a/web/src/app.vue b/web/src/app.vue index 515b016..664f6e0 100644 --- a/web/src/app.vue +++ b/web/src/app.vue @@ -5,8 +5,8 @@ + + + diff --git a/web/src/components/menuList.vue b/web/src/components/menuList.vue index 132953d..9f9269e 100644 --- a/web/src/components/menuList.vue +++ b/web/src/components/menuList.vue @@ -18,7 +18,7 @@