diff --git a/.gitignore b/.gitignore index 4790d71..cd65b26 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ server/app/db/* plan.md .env .env.local +.env-encrypt-key +*clear.js +local-script \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e3fcb17..2c265b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [3.0.0](https://github.com/chaos-zhu/easynode/releases) (2024-11-09) + +* 新增跳板机功能,支持选择多台机器跳转 +* 脚本库批量导出导入 +* 本地socket断开自动重连,无需手动重新连接 +* 支持脚本库模糊搜索功能 +* 分组添加实例数量标识 +* 优化登录逻辑 +* 默认登录有效期更改为当天有效 +* 修复一些小bug + ## [2.3.0](https://github.com/chaos-zhu/easynode/releases) (2024-10-24) * 重构本地数据库存储方式(性能提升一个level~) 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..c0b9c33 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 ./local-script/encrypt-file.js" }, "bugs": { "url": "https://github.com/chaos-zhu/easynode/issues" diff --git a/server/.env.template b/server/.env.template index 2aa3f6b..3b6d98b 100644 --- a/server/.env.template +++ b/server/.env.template @@ -2,4 +2,7 @@ DEBUG=1 # 访问IP限制 -allowedIPs=['127.0.0.1'] \ No newline at end of file +allowedIPs=['127.0.0.1'] + +# 激活PLUS功能的授权码 +PLUS_KEY= diff --git a/server/app/config/index.js b/server/app/config/index.js index 173d291..0c33ec7 100644 --- a/server/app/config/index.js +++ b/server/app/config/index.js @@ -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'), diff --git a/server/app/controller/host.js b/server/app/controller/host.js index 193df60..0c6ae07 100644 --- a/server/app/controller/host.js +++ b/server/app/controller/host.js @@ -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 }) @@ -108,7 +107,6 @@ async function removeHost({ res, request }) { let { body: { ids } } = request if (!Array.isArray(ids)) return res.fail({ msg: '参数错误' }) const numRemoved = await hostListDB.removeAsync({ _id: { $in: ids } }, { multi: true }) - // console.log('numRemoved: ', numRemoved) res.success({ data: `已移除,数量: ${ numRemoved }` }) } diff --git a/server/app/controller/plus.js b/server/app/controller/plus.js new file mode 100644 index 0000000..2203153 --- /dev/null +++ b/server/app/controller/plus.js @@ -0,0 +1 @@ +U2FsdGVkX19jtfvYuFxSS/crgNgJ1caplGxdOR2shVJsFvGpUPaNndWypMn/82hMNCkZALxbkig7yMLRalPRfl5OBNIgjZtM2AyLhJFlAaSj2+nx/FScOrLhIhJ5dHithW6cPcddXttFJRX/aP2Ybw5YxUZl8t3N58d4BJVDoJ5wXvwzAVb4LGEG3sfyVr2mJesXEPGPHBa6DCUrZA4fqoUYtmz/yLFtXpbV66HYAVIsL/tTyaDGD3mq99QvjHPcK3rMjSyTo9qCxH6AikWVXxFyaG+GuulhUQD3n2SRtdHezgjwVsFlsQCs5xTCDArC+kt8qepB4NIxGZIWvY3DUNdD1W9Jiq3nDMSfiWSu7k62FiyRvQpDiHgPE5RmnMOTlXWM0RatS6TTQVTHJCANYONhywVGaUn6RLOAXPl7C0ayJNIf1/rt4okNuvnU363pR7heARU52E/T10DMb5sfbG7lDebFXofZCeepYfYYUzkT4Zjwq8EHXB81rL9GE2gVFyMoIERaQKloFB+COf0NrGAHDdCuTllq/YmV3zWh8puJPQeO/ICy6z0b+bE0CRtNbkCicxBJrhZB1bmE4Kbhq/SNzdQwdH9vtuh+zSnM92HZdCAOuIZOxwMwZqdrYfR2Itr6yKw4V6iWa90wgNR5dj+32ovRkMP+jKbrPPT+r4AAJ0aVLDrOHdMGNldOfJqNAdw28TORKhAg6vK2VnVDmGfix8fLcZnhrikf6MEtZBMpNg7U5ftHjY9LbxwmAToocuIntdS3KfXOH0LHIsebNMqOWZGER6V/qP0xWdjksagFq/Wtze1CBQFV0HiOqzJG0TbYB+Fpa72Pwmuj3LmJaSryXsG4zm8L351RtCJ/qiXZe2wO3aih6TNZWyQZAcGBn4a/PXXar/aDvxIsee0s03e9YZ2Rsu81i4aeFua0zxUPS+K/gnrWFEChXBeNVJM0Cx4OVPmcuW4tG4xe2I2/QgCAsl9X4qDpS78jn5yMaa7EnqTd8ukzUVGERS66sPJBCz/h8PBvMbFBi+aHszJhZr/92sk2P8HGykAQ317WtcRXBsdOrChETPgQ7QKWVESCBlugdOS/pujKxQLvdeNyPG4nB5rGNXsm4XWtScKI72Tdx+r1Ayzn9DhPAfVRJarawzWUqzpDl7BIN/86dT7EGxfWxSLHj/rBNXhWESW/sA+SPuVz62VLztJQu6237xRI5wp+w97jwd4rSitlPZR1bDFRn4tsQ4ZxtJof1ZWE8qqHn0MnufKWmjumQxEDMSpwegPyLhesWPEazUoFZK6QhJCTOKBLxK3WRNDgHEljTjPDLC9Jry1tYJrVfGiP6lcIiB+08XoaPj95TUin7udZyIrA5lay0VPia3yTBwV3ozLs5WK80innYqtCmvbCjkLB4MdaWCVAadO6NpfXaNh5IudaINJVhYvAt9Z4TKpTzxg/PNhunk/Q3vGuS/x8FCjK \ No newline at end of file diff --git a/server/app/controller/scripts.js b/server/app/controller/scripts.js index 7993df3..5e0e00e 100644 --- a/server/app/controller/scripts.js +++ b/server/app/controller/scripts.js @@ -1,6 +1,8 @@ +const path = require('path') const localShellJson = require('../config/shell.json') const { randomStr } = require('../utils/tools') const { ScriptsDB } = require('../utils/db-class') +const decryptAndExecuteAsync = require('../utils/decrypt-file') const scriptsDB = new ScriptsDB().getInstance() let localShell = JSON.parse(JSON.stringify(localShellJson)).map((item) => { @@ -44,10 +46,28 @@ const removeScript = async ({ res, request }) => { res.success({ data: '移除成功' }) } +const batchRemoveScript = async ({ res, request }) => { + let { body: { ids } } = request + if (!Array.isArray(ids)) return res.fail({ msg: '参数错误' }) + const numRemoved = await scriptsDB.removeAsync({ _id: { $in: ids } }, { multi: true }) + res.success({ data: `批量移除成功,数量: ${ numRemoved }` }) +} + +const importScript = async ({ res, request }) => { + let { impScript } = (await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))) || {} + if (impScript) { + await impScript({ res, request }) + } else { + return res.fail({ data: false, msg: 'Plus专属功能,无法导入脚本!' }) + } +} + module.exports = { addScript, getScriptList, getLocalScriptList, updateScriptList, - removeScript + removeScript, + batchRemoveScript, + importScript } diff --git a/server/app/controller/ssh.js b/server/app/controller/ssh.js index 4a794c6..19d648d 100644 --- a/server/app/controller/ssh.js +++ b/server/app/controller/ssh.js @@ -1,10 +1,11 @@ +const path = require('path') const { RSADecryptAsync, AESEncryptAsync, 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 getSSHList({ res }) { - // console.log('get-host-list') let data = await credentialsDB.findAsync({}) data = data?.map(item => { const { name, authType, _id: id, date } = item @@ -83,10 +84,19 @@ const getCommand = async ({ res, request }) => { let hostInfo = await hostListDB.findAsync({}) let record = hostInfo?.find(item => item._id === hostId) consola.info('查询登录后执行的指令:', hostId) - if (!record) return res.fail({ data: false, msg: 'host not found' }) // host不存在 + if (!record) return res.fail({ data: false, msg: 'host not found' }) const { command } = record - if (!command) return res.success({ data: false }) // command不存在 - res.success({ data: command }) // 存在 + if (!command) return res.success({ data: false }) + res.success({ data: command }) +} + +const decryptPrivateKey = async ({ res, request }) => { + let { dePrivateKey } = (await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))) || {} + if (dePrivateKey) { + await dePrivateKey({ res, request }) + } else { + return res.fail({ data: false, msg: 'Plus专属功能,无法解密私钥!' }) + } } module.exports = { @@ -94,5 +104,6 @@ module.exports = { addSSH, updateSSH, removeSSH, - getCommand + getCommand, + decryptPrivateKey } diff --git a/server/app/controller/user.js b/server/app/controller/user.js index efc8af7..6a673e4 100644 --- a/server/app/controller/user.js +++ b/server/app/controller/user.js @@ -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,13 @@ const disableMFA2 = async ({ res }) => { res.success({ msg: 'success' }) } +const getPlusInfo = async ({ res }) => { + let data = await plusDB.findOneAsync({}) + delete data?._id + delete data?.decryptKey + res.success({ data, msg: 'success' }) +} + module.exports = { login, getpublicKey, @@ -172,5 +180,6 @@ module.exports = { getMFA2Status, getMFA2Code, enableMFA2, - disableMFA2 + disableMFA2, + getPlusInfo } diff --git a/server/app/main.js b/server/app/main.js index 976ea78..fc1dc6c 100644 --- a/server/app/main.js +++ b/server/app/main.js @@ -1,13 +1,13 @@ -const consola = require('consola') -global.consola = consola const { httpServer } = require('./server') const initDB = require('./db') const scheduleJob = require('./schedule') +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/router/routes.js b/server/app/router/routes.js index 2602965..1e01992 100644 --- a/server/app/router/routes.js +++ b/server/app/router/routes.js @@ -1,9 +1,9 @@ -const { getSSHList, addSSH, updateSSH, removeSSH, getCommand } = require('../controller/ssh') +const { getSSHList, addSSH, updateSSH, removeSSH, getCommand, decryptPrivateKey } = 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') +const { getScriptList, getLocalScriptList, addScript, updateScriptList, removeScript, batchRemoveScript, importScript } = require('../controller/scripts') const { getOnekeyRecord, removeOnekeyRecord } = require('../controller/onekey') const { getLog } = require('../controller/log') @@ -32,6 +32,11 @@ const ssh = [ method: 'get', path: '/command', controller: getCommand + }, + { + method: 'post', + path: '/decrypt-private-key', + controller: decryptPrivateKey } ] const host = [ @@ -101,6 +106,11 @@ const user = [ method: 'post', path: '/mfa2-disable', controller: disableMFA2 + }, + { + method: 'get', + path: '/plus-info', + controller: getPlusInfo } ] const notify = [ @@ -170,10 +180,20 @@ const scripts = [ path: '/script/:id', controller: removeScript }, + { + method: 'post', + path: '/batch-remove-script', + controller: batchRemoveScript + }, { method: 'put', path: '/script/:id', controller: updateScriptList + }, + { + method: 'post', + path: '/import-script', + controller: importScript } ] diff --git a/server/app/schedule/expired-notify.js b/server/app/schedule/expired-notify.js deleted file mode 100644 index 86c5b37..0000000 --- a/server/app/schedule/expired-notify.js +++ /dev/null @@ -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) -} diff --git a/server/app/schedule/index.js b/server/app/schedule/index.js index 60c8347..275dd64 100644 --- a/server/app/schedule/index.js +++ b/server/app/schedule/index.js @@ -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) } diff --git a/server/app/socket/plus.js b/server/app/socket/plus.js new file mode 100644 index 0000000..2133e84 --- /dev/null +++ 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 13eb9f2..7b29899 100644 --- a/server/app/socket/terminal.js +++ b/server/app/socket/terminal.js @@ -1,108 +1,101 @@ +const path = require('path') 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 { 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() -function createInteractiveShell(socket, sshClient) { +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) => { - sshClient.shell({ term: 'xterm-color' }, (err, stream) => { + 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()) }) .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 { connectByJumpHosts = null } = (await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))) || {} + 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 = connectByJumpHosts && (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 - .on('ready', async() => { + let closeNoticeFlag = false // 避免重复发送通知 + 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) - 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({ - ...authInfo - // debug: (info) => console.log(info) + ...targetConnectionOptions + // 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) } }) } @@ -111,11 +104,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地址不在白名单中') @@ -123,7 +120,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,47 +128,21 @@ 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 }) { - // 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) - sshClient?.end() - sshClient?.destroy() - sshClient = null - stream = null - setTimeout(async () => { - // 初始化新的SSH客户端对象 - sshClient = new SSHClient() - stream = await createTerminal(hostId, socket, sshClient) - 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) => { + socket.on('get_ping', async (ip) => { try { socket.emit('ping_data', await ping(ip, 2500)) } catch (error) { @@ -180,7 +151,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/db-class.js b/server/app/utils/db-class.js index 6c11aba..e3d802b 100644 --- a/server/app/utils/db-class.js +++ b/server/app/utils/db-class.js @@ -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 + } } \ No newline at end of file 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/tools.js b/server/app/utils/tools.js index e190530..d531bbd 100644 --- a/server/app/utils/tools.js +++ b/server/app/utils/tools.js @@ -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, diff --git a/server/index.js b/server/index.js index 110576d..b1e4be9 100644 --- a/server/index.js +++ b/server/index.js @@ -1,2 +1,4 @@ +const consola = require('consola') +global.consola = consola require('dotenv').config() require('./app/main.js') diff --git a/server/package.json b/server/package.json index ac75cb3..02a4726 100644 --- a/server/package.json +++ b/server/package.json @@ -1,58 +1,58 @@ -{ - "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" + }, + "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" + } +} 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..09663f6 100644 --- a/web/src/api/index.js +++ b/web/src/api/index.js @@ -19,12 +19,15 @@ 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 } }) }, + decryptPrivateKey(data) { + return axios({ url: '/decrypt-private-key', method: 'post', data }) + }, getHostList() { return axios({ url: '/host-list', method: 'get' }) }, @@ -88,8 +91,11 @@ export default { deleteGroup(id) { return axios({ url: `/group/${ id }`, method: 'delete' }) }, - getScriptList() { - return axios({ url: '/script', method: 'get' }) + getScriptList(params = {}) { + return axios({ url: '/script', method: 'get', params }) + }, + importScript(data) { + return axios({ url: '/import-script', method: 'post', data }) }, getLocalScriptList() { return axios({ url: '/local-script', method: 'get' }) @@ -103,6 +109,9 @@ export default { deleteScript(id) { return axios({ url: `/script/${ id }`, method: 'delete' }) }, + batchRemoveScript(data) { + return axios({ url: '/batch-remove-script', method: 'post', data }) + }, getOnekeyRecord() { return axios({ url: '/onekey', method: 'get' }) }, 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 @@ \ No newline at end of file diff --git a/web/src/views/scripts/index.vue b/web/src/views/scripts/index.vue index 3e6cb15..af60728 100644 --- a/web/src/views/scripts/index.vue +++ b/web/src/views/scripts/index.vue @@ -1,9 +1,52 @@ + +
+ +
+ + + \ No newline at end of file diff --git a/web/src/views/server/components/host-form.vue b/web/src/views/server/components/host-form.vue index 293a7ff..eb23696 100644 --- a/web/src/views/server/components/host-form.vue +++ b/web/src/views/server/components/host-form.vue @@ -155,9 +155,9 @@ prop="credential" label="凭据" > - + {{ host }} - 复制 + 复制 - +