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).
+
+
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 @@