✨ plus&功能重构
This commit is contained in:
parent
1b3b2892d0
commit
c55d3bddd6
2
.gitignore
vendored
2
.gitignore
vendored
@ -11,3 +11,5 @@ server/app/db/*
|
||||
plan.md
|
||||
.env
|
||||
.env.local
|
||||
.env-encrypt-key
|
||||
*clear.js
|
||||
|
@ -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).
|
||||
|
||||

|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
// })
|
||||
const appDir = path.join(__dirname, '../server')
|
||||
|
||||
encryptPlusClearFiles(appDir)
|
||||
.then(() => {
|
||||
console.log(`${version} 版本加密完成!`)
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('程序执行出错:', error)
|
||||
})
|
21
script/update-version.js
Normal file
21
script/update-version.js
Normal file
@ -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}`);
|
||||
});
|
@ -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' })
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
module.exports = useLog()
|
||||
|
||||
// 可以先测试一下日志是否正常工作
|
||||
logger.info('日志系统启动')
|
@ -0,0 +1 @@
|
||||
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
|
@ -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
|
||||
|
53
server/app/utils/decrypt-file.js
Normal file
53
server/app/utils/decrypt-file.js
Normal file
@ -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
|
93
server/app/utils/get-plus.js
Normal file
93
server/app/utils/get-plus.js
Normal file
@ -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
|
@ -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": "",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "2.3.0",
|
||||
"version": "3.0.0",
|
||||
"description": "easynode-web",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -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 } })
|
||||
},
|
||||
|
@ -5,8 +5,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, getCurrentInstance } from 'vue'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
import { ref, getCurrentInstance } from 'vue'
|
||||
const { proxy: { $store } } = getCurrentInstance()
|
||||
|
||||
const locale = ref(zhCn)
|
||||
|
BIN
web/src/assets/plus.png
Normal file
BIN
web/src/assets/plus.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.9 KiB |
42
web/src/components/common/PlusSupportTip.vue
Normal file
42
web/src/components/common/PlusSupportTip.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<el-tooltip
|
||||
:disabled="isPlusActive"
|
||||
placement="top"
|
||||
>
|
||||
<template #content>
|
||||
<div class="plus_support_tip">
|
||||
此功能需要激活Plus后使用,
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
link
|
||||
@click="handlePlusSupport"
|
||||
>
|
||||
去激活
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<slot />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, getCurrentInstance } from 'vue'
|
||||
|
||||
const { proxy: { $store } } = getCurrentInstance()
|
||||
|
||||
const isPlusActive = computed(() => $store.isPlusActive)
|
||||
|
||||
const handlePlusSupport = () => {
|
||||
window.open('https://en.221022.xyz/buy-plus', '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.plus_support_tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@ -18,7 +18,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, markRaw, getCurrentInstance, computed, watchEffect, defineEmits } from 'vue'
|
||||
import { reactive, markRaw, getCurrentInstance, computed, watchEffect } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import {
|
||||
Menu as IconMenu,
|
||||
|
@ -2,7 +2,9 @@
|
||||
<div class="top_bar_container">
|
||||
<div class="bar_wrap">
|
||||
<div class="mobile_menu_btn">
|
||||
<el-icon @click="handleCollapse"><Fold /></el-icon>
|
||||
<el-icon @click="handleCollapse">
|
||||
<Fold />
|
||||
</el-icon>
|
||||
</div>
|
||||
<h2>{{ title }}</h2>
|
||||
<el-switch
|
||||
@ -21,7 +23,12 @@
|
||||
关于 <span class="new_version">{{ isNew ? `(新版本可用)` : '' }}</span>
|
||||
</el-button>
|
||||
<el-dropdown trigger="click">
|
||||
<span class="username"><el-icon><User /></el-icon> {{ user }}</span>
|
||||
<span class="username_wrap">
|
||||
<el-icon>
|
||||
<User />
|
||||
</el-icon>
|
||||
<span class="username">{{ user }}</span>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="handleLogout">
|
||||
@ -30,6 +37,87 @@
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
<el-popover placement="left" :width="320" trigger="hover">
|
||||
<template #reference>
|
||||
<img
|
||||
class="plus_icon"
|
||||
src="@/assets/plus.png"
|
||||
alt="PLUS"
|
||||
:style="{ filter: isPlusActive ? 'grayscale(0%)' : 'grayscale(100%)' }"
|
||||
>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="plus_content_wrap">
|
||||
<!-- Plus 激活状态信息 -->
|
||||
<div v-if="isPlusActive" class="plus_status">
|
||||
<div class="status_header">
|
||||
<el-icon>
|
||||
<CircleCheckFilled />
|
||||
</el-icon>
|
||||
<span>Plus专属功能已激活</span>
|
||||
</div>
|
||||
<div class="status_info">
|
||||
<div class="info_item">
|
||||
<span class="label">到期时间:</span>
|
||||
<span class="value holder">{{ plusInfo.expiryDate }}</span>
|
||||
</div>
|
||||
<div class="info_item">
|
||||
<span class="label">授权IP数:</span>
|
||||
<span class="value">{{ plusInfo.maxIPs }}</span>
|
||||
</div>
|
||||
<div class="info_item">
|
||||
<span class="label">已授权IP数:</span>
|
||||
<span class="value">{{ plusInfo.usedIPCount }}</span>
|
||||
</div>
|
||||
<div class="info_item ip_list">
|
||||
<span class="label">已授权IP:</span>
|
||||
<div class="ip_tags">
|
||||
<el-tag
|
||||
v-for="ip in plusInfo.usedIPs"
|
||||
:key="ip"
|
||||
size="small"
|
||||
class="ip_tag"
|
||||
>
|
||||
{{ ip }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="plus_benefits" :class="{ active: isPlusActive }" @click="handlePlus">
|
||||
<span v-if="!isPlusActive" class="support_btn" @click="handlePlusSupport">去支持</span>
|
||||
<div class="benefits_header">
|
||||
<el-icon>
|
||||
<el-icon><StarFilled /></el-icon>
|
||||
</el-icon>
|
||||
<span>Plus功能介绍</span>
|
||||
</div>
|
||||
<div class="current_benefits">
|
||||
<div v-for="plusFeature in plusFeatures" :key="plusFeature" class="benefit_item">
|
||||
<el-icon>
|
||||
<Star />
|
||||
</el-icon>
|
||||
<span>{{ plusFeature }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="coming_soon">
|
||||
<div class="soon_header">开发中的PLUS功能</div>
|
||||
<div class="current_benefits">
|
||||
<div v-for="soonFeature in soonFeatures" :key="soonFeature" class="benefit_item">
|
||||
<el-icon>
|
||||
<Star />
|
||||
</el-icon>
|
||||
<span>{{ soonFeature }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
@ -44,18 +132,36 @@
|
||||
<p>当前版本: {{ currentVersion }} <span v-show="!isNew">(最新)</span> </p>
|
||||
<p v-if="checkVersionErr" class="conspicuous">Error:版本更新检测失败(版本检测API需要外网环境)</p>
|
||||
<p v-if="isNew" class="conspicuous">
|
||||
新版本可用: {{ latestVersion }} -> <a class="link" href="https://github.com/chaos-zhu/easynode/releases" target="_blank">https://github.com/chaos-zhu/easynode/releases</a>
|
||||
新版本可用: {{ latestVersion }} -> <a
|
||||
class="link"
|
||||
href="https://github.com/chaos-zhu/easynode/releases"
|
||||
target="_blank"
|
||||
>https://github.com/chaos-zhu/easynode/releases</a>
|
||||
</p>
|
||||
<p>更新日志:<a class="link" href="https://github.com/chaos-zhu/easynode/blob/main/CHANGELOG.md" target="_blank">https://github.com/chaos-zhu/easynode/blob/main/CHANGELOG.md</a></p>
|
||||
<p>开源仓库: <a class="link" href="https://github.com/chaos-zhu/easynode" target="_blank">https://github.com/chaos-zhu/easynode</a></p>
|
||||
<p>作者: <a class="link" href="https://github.com/chaos-zhu" target="_blank">chaoszhu</a></p>
|
||||
<p>tg更新通知:<a class="link" href="https://t.me/easynode_notify" target="_blank">https://t.me/easynode_notify</a></p>
|
||||
<p>
|
||||
打赏: EasyNode开源且无任何收费,如果您认为此项目帮到了您, 您可以请我喝杯阔乐(记得留个备注)~
|
||||
更新日志:<a
|
||||
class="link"
|
||||
href="https://github.com/chaos-zhu/easynode/blob/main/CHANGELOG.md"
|
||||
target="_blank"
|
||||
>https://github.com/chaos-zhu/easynode/blob/main/CHANGELOG.md</a>
|
||||
</p>
|
||||
<p class="qrcode">
|
||||
<img src="@/assets/wx.jpg" alt="">
|
||||
<p>
|
||||
tg更新通知:<a class="link" href="https://t.me/easynode_notify" target="_blank">https://t.me/easynode_notify</a>
|
||||
</p>
|
||||
<p style="line-height: 2;letter-spacing: 1px;">
|
||||
<strong style="color: #F56C6C;font-weight: 600;">PLUS说明:</strong><br>
|
||||
<strong>EasyNode</strong>最初是一个简单的Web终端工具,随着用户群的不断扩大,功能需求也日益增长,为了实现大家的功能需求,我投入了大量的业余时间进行开发和维护。
|
||||
一直在为爱发电,渐渐的也没了开发的动力。
|
||||
<br>
|
||||
为了项目的可持续发展,从<strong>3.0.0</strong>版本开始推出了<strong>PLUS</strong>版本,具体特性鼠标悬浮右上角PLUS图标查看,后续特性功能开发也会优先在<strong>PLUS</strong>版本中实现,但即使不升级到<strong>PLUS</strong>,也不会影响到<strong>EasyNode</strong>的基础功能使用【注意: 暂不支持纯内网用户激活PLUS功能】。
|
||||
<br>
|
||||
<span style="text-decoration: underline;">
|
||||
为了感谢前期赞赏过的用户, 在<strong>PLUS</strong>功能正式发布前,所有进行过赞赏的用户,无论金额大小,均可联系 TG: <a class="link" href="https://t.me/chaoszhu" target="_blank">@chaoszhu</a> 凭打赏记录获取永久<strong>PLUS</strong>授权码。
|
||||
</span>
|
||||
</p>
|
||||
<div v-if="!isPlusActive" class="about_footer">
|
||||
<el-button type="primary" @click="handlePlusSupport">去支持</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
@ -76,7 +182,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, getCurrentInstance, computed } from 'vue'
|
||||
import { User, Sunny, Moon, Fold } from '@element-plus/icons-vue'
|
||||
import { User, Sunny, Moon, Fold, CircleCheckFilled, Star, StarFilled } from '@element-plus/icons-vue'
|
||||
import packageJson from '../../package.json'
|
||||
import MenuList from './menuList.vue'
|
||||
|
||||
@ -88,9 +194,25 @@ const currentVersion = ref(`v${ packageJson.version }`)
|
||||
const latestVersion = ref(null)
|
||||
const menuCollapse = ref(false)
|
||||
|
||||
const plusFeatures = [
|
||||
'跳板机功能,拯救被墙实例与龟速终端输入',
|
||||
'本地socket断开自动重连,无需手动重新连接',
|
||||
'提出的功能需求享有更高的开发优先级',
|
||||
]
|
||||
const soonFeatures = [
|
||||
'终端分屏功能(plus)',
|
||||
'终端脚本变量支持(plus)',
|
||||
'脚本库批量导出导入(plus)',
|
||||
'密码密钥解密功能(plus)',
|
||||
'系统操作日志审计(plus)',
|
||||
]
|
||||
|
||||
const isNew = computed(() => latestVersion.value && latestVersion.value !== currentVersion.value)
|
||||
const user = computed(() => $store.user)
|
||||
const title = computed(() => $store.title)
|
||||
const plusInfo = computed(() => $store.plusInfo)
|
||||
const isPlusActive = computed(() => $store.isPlusActive)
|
||||
|
||||
const isDark = computed({
|
||||
get: () => $store.isDark,
|
||||
set: (isDark) => {
|
||||
@ -108,6 +230,10 @@ const handleLogout = () => {
|
||||
$router.push('/login')
|
||||
}
|
||||
|
||||
const handlePlusSupport = () => {
|
||||
window.open('https://en.221022.xyz/buy-plus', '_blank')
|
||||
}
|
||||
|
||||
async function checkLatestVersion() {
|
||||
const timeout = 3000
|
||||
try {
|
||||
@ -151,47 +277,186 @@ checkLatestVersion()
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
|
||||
.bar_wrap {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.dark_switch {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.about_btn {
|
||||
margin-right: 15px;
|
||||
font-size: 14px;
|
||||
|
||||
.new_version {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
.username {
|
||||
|
||||
.username_wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.username {
|
||||
cursor: pointer;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.plus_icon {
|
||||
margin-left: 15px;
|
||||
width: 35px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.about_content {
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin: 15px 0;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 35px;
|
||||
line-height: 1.8;
|
||||
margin: 12px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
.qrcode {
|
||||
text-align: center;
|
||||
img {
|
||||
width: 250px;
|
||||
|
||||
.link {
|
||||
color: #409EFF;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.conspicuous {
|
||||
color: red;
|
||||
color: #F56C6C;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.about_footer {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.plus_content_wrap {
|
||||
.plus_status {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.status_header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #67c23a;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.el-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.status_info {
|
||||
.info_item {
|
||||
display: flex;
|
||||
margin: 5px 0;
|
||||
font-size: 13px;
|
||||
|
||||
.label {
|
||||
color: #909399;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.holder {
|
||||
color: #EED183;
|
||||
}
|
||||
|
||||
&.ip_list {
|
||||
flex-direction: column;
|
||||
|
||||
.ip_tags {
|
||||
margin-top: 5px;
|
||||
|
||||
.ip_tag {
|
||||
margin: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plus_benefits {
|
||||
position: relative;
|
||||
|
||||
.support_btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
padding: 4px 12px;
|
||||
background-color: #409eff;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: #66b1ff;
|
||||
}
|
||||
}
|
||||
|
||||
.benefits_header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.el-icon {
|
||||
color: #e6a23c;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.current_benefits {
|
||||
margin-bottom: 15px;
|
||||
|
||||
.benefit_item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 8px 0;
|
||||
font-size: 13px;
|
||||
|
||||
.el-icon {
|
||||
margin-right: 5px;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.coming_soon {
|
||||
.soon_header {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,10 +40,13 @@ const useStore = defineStore({
|
||||
background: 'linear-gradient(-225deg, #CBBACC 0%, #2580B3 100%)',
|
||||
quickCopy: isHttps(),
|
||||
quickPaste: isHttps(),
|
||||
autoReconnect: true,
|
||||
autoExecuteScript: false
|
||||
},
|
||||
...(localStorage.getItem('terminalConfig') ? JSON.parse(localStorage.getItem('terminalConfig')) : {})
|
||||
}
|
||||
},
|
||||
plusInfo: {},
|
||||
isPlusActive: false
|
||||
}),
|
||||
actions: {
|
||||
async setJwtToken(token, isSession = true) {
|
||||
@ -68,6 +71,7 @@ const useStore = defineStore({
|
||||
await this.getHostList()
|
||||
await this.getSSHList()
|
||||
await this.getScriptList()
|
||||
await this.getPlusInfo()
|
||||
this.wsClientsStatus()
|
||||
},
|
||||
async getHostList() {
|
||||
@ -96,6 +100,20 @@ const useStore = defineStore({
|
||||
const { data: localScriptList } = await $api.getLocalScriptList()
|
||||
this.$patch({ localScriptList })
|
||||
},
|
||||
async getPlusInfo() {
|
||||
const { data: plusInfo } = await $api.getPlusInfo()
|
||||
if (plusInfo?.expiryDate) {
|
||||
const isPlusActive = new Date(plusInfo.expiryDate) > new Date()
|
||||
this.$patch({ isPlusActive })
|
||||
if (!isPlusActive) {
|
||||
this.setTerminalSetting({ autoReconnect: false })
|
||||
return
|
||||
}
|
||||
plusInfo.expiryDate = dayjs(plusInfo.expiryDate).format('YYYY-MM-DD')
|
||||
plusInfo.expiryDate?.startsWith('9999') && (plusInfo.expiryDate = '永久授权')
|
||||
}
|
||||
this.$patch({ plusInfo })
|
||||
},
|
||||
setTerminalSetting(setTarget = {}) {
|
||||
let newConfig = { ...this.terminalConfig, ...setTarget }
|
||||
localStorage.setItem('terminalConfig', JSON.stringify(newConfig))
|
||||
|
@ -1,13 +1,11 @@
|
||||
// 终端连接状态
|
||||
export const terminalStatus = {
|
||||
CONNECTING: 'connecting',
|
||||
RECONNECTING: 'reconnecting',
|
||||
CONNECT_FAIL: 'connect_fail',
|
||||
CONNECT_SUCCESS: 'connect_success'
|
||||
}
|
||||
export const terminalStatusList = [
|
||||
{ value: terminalStatus.CONNECTING, label: '连接中', color: '#FFA500' },
|
||||
{ value: terminalStatus.RECONNECTING, label: '重连中', color: '#FFA500' },
|
||||
{ value: terminalStatus.CONNECT_FAIL, label: '连接失败', color: '#DC3545' },
|
||||
{ value: terminalStatus.CONNECT_SUCCESS, label: '已连接', color: '#28A745' },
|
||||
]
|
||||
|
@ -90,7 +90,7 @@ const expireEnum = reactive({
|
||||
CURRENT_DAY: 'current_day',
|
||||
THREE_DAY: 'three_day'
|
||||
})
|
||||
const expireTime = ref(expireEnum.ONE_SESSION)
|
||||
const expireTime = ref(expireEnum.CURRENT_DAY)
|
||||
const loginFormRefs = ref(null)
|
||||
const notKey = ref(false)
|
||||
const loading = ref(false)
|
||||
|
@ -155,9 +155,9 @@
|
||||
prop="credential"
|
||||
label="凭据"
|
||||
>
|
||||
<el-select v-model="hostForm.credential" class="credential_select" placeholder="">
|
||||
<el-select v-model="hostForm.credential" placeholder="">
|
||||
<template #empty>
|
||||
<div class="empty_credential">
|
||||
<div class="empty_text">
|
||||
<span>无凭据数据,</span>
|
||||
<el-button type="primary" link @click="toCredentials">
|
||||
去添加
|
||||
@ -170,7 +170,7 @@
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
>
|
||||
<div class="auth_type_wrap">
|
||||
<div class="select_warp">
|
||||
<span>{{ item.name }}</span>
|
||||
<span class="auth_type_text">
|
||||
{{ item.authType === 'privateKey' ? '密钥' : '密码' }}
|
||||
@ -179,7 +179,37 @@
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item key="command" prop="command" label="执行指令">
|
||||
<el-form-item
|
||||
key="jumpHosts"
|
||||
prop="jumpHosts"
|
||||
label="跳板机"
|
||||
>
|
||||
<PlusSupportTip>
|
||||
<el-select
|
||||
v-model="hostForm.jumpHosts"
|
||||
placeholder="支持多选,跳板机连接顺序从前到后"
|
||||
multiple
|
||||
:disabled="!isPlusActive"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="empty_text">
|
||||
<span>无可用跳板机器</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-option
|
||||
v-for="item in confHostList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
>
|
||||
<div class="select_wrap">
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</PlusSupportTip>
|
||||
</el-form-item>
|
||||
<el-form-item key="command" prop="command" label="登录指令">
|
||||
<el-input
|
||||
v-model="hostForm.command"
|
||||
type="textarea"
|
||||
@ -263,6 +293,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, getCurrentInstance, nextTick } from 'vue'
|
||||
import PlusSupportTip from '@/components/common/PlusSupportTip.vue'
|
||||
import { RSAEncrypt, AESEncrypt, randomStr } from '@utils/index.js'
|
||||
|
||||
const { proxy: { $api, $router, $message, $store } } = getCurrentInstance()
|
||||
@ -306,7 +337,8 @@ const formField = {
|
||||
expiredNotify: false,
|
||||
consoleUrl: '',
|
||||
remark: '',
|
||||
command: ''
|
||||
command: '',
|
||||
jumpHosts: []
|
||||
}
|
||||
|
||||
let hostForm = ref({ ...formField })
|
||||
@ -323,7 +355,8 @@ const rules = computed(() => {
|
||||
name: { required: !isBatchModify.value, message: '输入实例别名', trigger: 'change' },
|
||||
host: { required: !isBatchModify.value, message: '输入IP/域名', trigger: 'change' },
|
||||
port: { required: !isBatchModify.value, type: 'number', message: '输入ssh端口', trigger: 'change' },
|
||||
clientPort: { required: false, type: 'number', message: '输入ssh端口', trigger: 'change' },
|
||||
clientPort: { required: false, type: 'number' },
|
||||
jumpHosts: { required: false, type: 'array' },
|
||||
index: { required: !isBatchModify.value, type: 'number', message: '输入数字', trigger: 'change' },
|
||||
// password: [{ required: hostForm.authType === 'password', trigger: 'change' },],
|
||||
// privateKey: [{ required: hostForm.authType === 'privateKey', trigger: 'change' },],
|
||||
@ -333,6 +366,7 @@ const rules = computed(() => {
|
||||
remark: { required: false }
|
||||
}
|
||||
})
|
||||
const isPlusActive = computed(() => $store.isPlusActive)
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.show,
|
||||
@ -345,6 +379,10 @@ const title = computed(() => {
|
||||
|
||||
let groupList = computed(() => $store.groupList)
|
||||
let sshList = computed(() => $store.sshList)
|
||||
let hostList = computed(() => $store.hostList)
|
||||
let confHostList = computed(() => {
|
||||
return hostList.value?.filter(item => item.isConfig)
|
||||
})
|
||||
|
||||
const setDefaultData = () => {
|
||||
if (!defaultData.value) return
|
||||
@ -356,7 +394,7 @@ const setDefaultData = () => {
|
||||
|
||||
const setBatchDefaultData = () => {
|
||||
if (!isBatchModify.value) return
|
||||
Object.assign(hostForm.value, { ...formField }, { group: '', port: '', username: '', authType: '', clientPort: '' })
|
||||
Object.assign(hostForm.value, { ...formField }, { group: '', port: '', username: '', authType: '', clientPort: '', jumpHosts: [] })
|
||||
}
|
||||
const handleOpen = async () => {
|
||||
setDefaultData()
|
||||
@ -472,13 +510,13 @@ const handleSave = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.empty_credential {
|
||||
.empty_text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.auth_type_wrap {
|
||||
.select_warp {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -15,9 +15,11 @@
|
||||
</template>
|
||||
<span style="margin-right: 10px;">{{ host }}</span>
|
||||
<template v-if="pingMs">
|
||||
<span class="host-ping" :style="{backgroundColor: handlePingColor(pingMs)}">{{ pingMs }}ms</span>
|
||||
<el-tooltip effect="dark" content="该值为EasyNode服务端主机到目标主机的ping值" placement="bottom">
|
||||
<span class="host-ping" :style="{backgroundColor: handlePingColor(pingMs)}">{{ pingMs }}ms</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-tag size="small" style="cursor: pointer;margin-left: 15px;" @click="handleCopy">复制</el-tag>
|
||||
<el-tag size="small" style="cursor: pointer;margin-left: 10px;" @click="handleCopy">复制</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
|
@ -66,6 +66,22 @@
|
||||
label-width="100px"
|
||||
:show-message="false"
|
||||
>
|
||||
<el-form-item label="自动重连" prop="autoReconnect">
|
||||
<PlusSupportTip>
|
||||
<span>
|
||||
<el-switch
|
||||
v-model="autoReconnect"
|
||||
class="swtich"
|
||||
inline-prompt
|
||||
:disabled="!isPlusActive"
|
||||
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
|
||||
active-text="开启"
|
||||
inactive-text="关闭"
|
||||
/>
|
||||
<span class="plus_support_tip_text">(Plus专属功能)</span>
|
||||
</span>
|
||||
</PlusSupportTip>
|
||||
</el-form-item>
|
||||
<el-form-item label="选中复制" prop="quickCopy">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
@ -129,6 +145,7 @@
|
||||
import { computed, getCurrentInstance } from 'vue'
|
||||
import themeList from 'xterm-theme'
|
||||
import useMobileWidth from '@/composables/useMobileWidth'
|
||||
import PlusSupportTip from '@/components/common/PlusSupportTip.vue'
|
||||
const { proxy: { $store } } = getCurrentInstance()
|
||||
|
||||
const props = defineProps({
|
||||
@ -162,6 +179,10 @@ const quickCopy = computed({
|
||||
get: () => $store.terminalConfig.quickCopy,
|
||||
set: (newVal) => $store.setTerminalSetting({ quickCopy: newVal })
|
||||
})
|
||||
const autoReconnect = computed({
|
||||
get: () => $store.terminalConfig.autoReconnect,
|
||||
set: (newVal) => $store.setTerminalSetting({ autoReconnect: newVal })
|
||||
})
|
||||
const quickPaste = computed({
|
||||
get: () => $store.terminalConfig.quickPaste,
|
||||
set: (newVal) => $store.setTerminalSetting({ quickPaste: newVal })
|
||||
@ -170,6 +191,7 @@ const autoExecuteScript = computed({
|
||||
get: () => $store.terminalConfig.autoExecuteScript,
|
||||
set: (newVal) => $store.setTerminalSetting({ autoExecuteScript: newVal })
|
||||
})
|
||||
const isPlusActive = computed(() => $store.isPlusActive)
|
||||
|
||||
const changeBackground = (item) => {
|
||||
background.value = item || ''
|
||||
@ -225,5 +247,8 @@ const changeBackground = (item) => {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.plus_support_tip_text {
|
||||
margin-left: 5px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
</style>
|
@ -24,7 +24,7 @@ import socketIo from 'socket.io-client'
|
||||
import themeList from 'xterm-theme'
|
||||
import { terminalStatus } from '@/utils/enum'
|
||||
|
||||
const { CONNECTING, RECONNECTING, CONNECT_SUCCESS, CONNECT_FAIL } = terminalStatus
|
||||
const { CONNECTING, CONNECT_SUCCESS, CONNECT_FAIL } = terminalStatus
|
||||
|
||||
const { io } = socketIo
|
||||
const { proxy: { $api, $store, $serviceURI, $notification, $router, $message } } = getCurrentInstance()
|
||||
@ -49,12 +49,11 @@ const emit = defineEmits(['inputCommand', 'cdCommand', 'ping-data', 'reset-long-
|
||||
const socket = ref(null)
|
||||
// const commandHistoryList = ref([])
|
||||
const term = ref(null)
|
||||
const command = ref('')
|
||||
const initCommand = ref('')
|
||||
const timer = ref(null)
|
||||
const pingTimer = ref(null)
|
||||
const fitAddon = ref(null)
|
||||
// const searchBar = ref(null)
|
||||
const hasRegisterEvent = ref(false)
|
||||
|
||||
const socketConnected = ref(false)
|
||||
const curStatus = ref(CONNECTING)
|
||||
@ -72,6 +71,8 @@ const menuCollapse = computed(() => $store.menuCollapse)
|
||||
const quickCopy = computed(() => $store.terminalConfig.quickCopy)
|
||||
const quickPaste = computed(() => $store.terminalConfig.quickPaste)
|
||||
const autoExecuteScript = computed(() => $store.terminalConfig.autoExecuteScript)
|
||||
const autoReconnect = computed(() => $store.terminalConfig.autoReconnect)
|
||||
const isPlusActive = computed(() => $store.isPlusActive)
|
||||
const isLongPressCtrl = computed(() => props.longPressCtrl)
|
||||
const isLongPressAlt = computed(() => props.longPressAlt)
|
||||
|
||||
@ -91,7 +92,6 @@ watch(theme, () => {
|
||||
watch(fontSize, () => {
|
||||
nextTick(() => {
|
||||
terminal.value.options.fontSize = fontSize.value
|
||||
// fitAddon.value.fit()
|
||||
handleResize()
|
||||
})
|
||||
})
|
||||
@ -116,14 +116,16 @@ watch(curStatus, () => {
|
||||
|
||||
const getCommand = async () => {
|
||||
let { data } = await $api.getCommand(hostId.value)
|
||||
if (data) command.value = data
|
||||
if (data) initCommand.value = data
|
||||
}
|
||||
|
||||
const connectIO = () => {
|
||||
curStatus.value = CONNECTING
|
||||
socket.value = io($serviceURI, {
|
||||
path: '/terminal',
|
||||
forceNew: false,
|
||||
reconnectionAttempts: 1
|
||||
reconnection: false,
|
||||
reconnectionAttempts: 0
|
||||
})
|
||||
socket.value.on('connect', () => {
|
||||
console.log('/terminal socket已连接:', hostId.value)
|
||||
@ -131,20 +133,14 @@ const connectIO = () => {
|
||||
socketConnected.value = true
|
||||
socket.value.emit('create', { hostId: hostId.value, token: token.value })
|
||||
socket.value.on('connect_terminal_success', () => {
|
||||
if (hasRegisterEvent.value) return // 以下事件连接成功后仅可注册一次, 否则会多次触发. 除非socket重连
|
||||
hasRegisterEvent.value = true
|
||||
|
||||
socket.value.on('output', (str) => {
|
||||
term.value.write(str)
|
||||
terminalText.value += str
|
||||
})
|
||||
|
||||
socket.value.on('connect_shell_success', () => {
|
||||
curStatus.value = CONNECT_SUCCESS
|
||||
onResize()
|
||||
onFindText()
|
||||
onWebLinks()
|
||||
if (command.value) socket.value.emit('input', command.value + '\n')
|
||||
shellResize()
|
||||
if (initCommand.value) socket.value.emit('input', initCommand.value + '\n')
|
||||
})
|
||||
|
||||
// socket.value.on('terminal_command_history', (data) => {
|
||||
@ -155,7 +151,7 @@ const connectIO = () => {
|
||||
|
||||
if (pingTimer.value) clearInterval(pingTimer.value)
|
||||
pingTimer.value = setInterval(() => {
|
||||
socket.value.emit('get_ping', host.value)
|
||||
socket.value?.emit('get_ping', host.value)
|
||||
}, 3000)
|
||||
socket.value.emit('get_ping', host.value) // 获取服务端到客户端的ping值
|
||||
socket.value.on('ping_data', (pingMs) => {
|
||||
@ -167,50 +163,79 @@ const connectIO = () => {
|
||||
$router.push('/login')
|
||||
})
|
||||
|
||||
socket.value.on('terminal_print_info', (msg) => {
|
||||
term.value.write(`${ msg }\r\n`)
|
||||
})
|
||||
|
||||
socket.value.on('connect_close', () => {
|
||||
if (curStatus.value === CONNECT_FAIL) return // 连接失败不需要自动重连
|
||||
curStatus.value = RECONNECTING
|
||||
console.warn('连接断开,3秒后自动重连: ', hostId.value)
|
||||
term.value.write('\r\n连接断开,3秒后自动重连...\r\n')
|
||||
socket.value.emit('reconnect_terminal')
|
||||
})
|
||||
|
||||
socket.value.on('reconnect_terminal_success', () => {
|
||||
curStatus.value = CONNECT_SUCCESS
|
||||
})
|
||||
|
||||
socket.value.on('create_fail', (message) => {
|
||||
curStatus.value = CONNECT_FAIL
|
||||
console.error('n创建失败:', hostId.value, message)
|
||||
term.value.write(`\r\n创建失败: ${ message }\r\n`)
|
||||
term.value.write('\r\n\x1b[91m终端主动断开连接, 回车重新发起连接\x1b[0m')
|
||||
})
|
||||
|
||||
socket.value.on('connect_fail', (message) => {
|
||||
socket.value.on('connect_terminal_fail', (message) => {
|
||||
curStatus.value = CONNECT_FAIL
|
||||
console.error('连接失败:', hostId.value, message)
|
||||
term.value.write(`\r\n连接失败: ${ message }\r\n`)
|
||||
term.value.write(`\r\n\x1b[91m连接终端失败: ${ message }, 回车重新发起连接\x1b[0m`)
|
||||
})
|
||||
|
||||
socket.value.on('create_terminal_fail', (message) => {
|
||||
curStatus.value = CONNECT_FAIL
|
||||
term.value.write(`\r\n\x1b[91m创建终端失败: ${ message }, 回车重新发起连接\x1b[0m`)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
socket.value.on('disconnect', () => {
|
||||
console.warn('terminal websocket 连接断开')
|
||||
socket.value.removeAllListeners() // 取消所有监听
|
||||
// socket.value.off('output') // 取消output监听,取消onData输入监听,重新注册
|
||||
curStatus.value = CONNECT_FAIL
|
||||
socketConnected.value = false
|
||||
term.value.write('\r\nError: 与面板socket连接断开。请关闭此tab,并检查本地与面板连接是否稳定\r\n')
|
||||
socket.value.on('disconnect', (reason) => {
|
||||
console.warn('terminal websocket 连接断开:', reason)
|
||||
switch (reason) {
|
||||
case 'io server disconnect':
|
||||
reconnectTerminal(true, '服务端主动断开连接')
|
||||
break
|
||||
// case 'io client disconnect': // 客户端主动断开连接
|
||||
// break
|
||||
case 'transport close':
|
||||
reconnectTerminal(true, '本地网络连接异常')
|
||||
break
|
||||
case 'transport error':
|
||||
reconnectTerminal(true, '建立连接错误')
|
||||
break
|
||||
case 'parse error':
|
||||
reconnectTerminal(true, '数据解析错误')
|
||||
break
|
||||
default:
|
||||
reconnectTerminal(true, '连接意外断开')
|
||||
}
|
||||
})
|
||||
|
||||
socket.value.on('connect_error', (err) => {
|
||||
console.error('terminal websocket 连接错误:', err)
|
||||
console.error('EasyNode服务端连接错误:', err)
|
||||
term.value.write('\r\n\x1b[91mError: 连接失败,请检查EasyNode服务端是否正常\x1b[0m \r\n')
|
||||
$notification({
|
||||
title: '终端连接失败',
|
||||
message: '请检查socket服务是否正常',
|
||||
title: '连接失败',
|
||||
message: '请检查EasyNode服务端是否正常',
|
||||
type: 'error'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const reconnectTerminal = (isCommonTips = false, tips) => {
|
||||
socket.value.removeAllListeners()
|
||||
socket.value.close()
|
||||
socket.value = null
|
||||
curStatus.value = CONNECT_FAIL
|
||||
socketConnected.value = false
|
||||
if (isCommonTips) {
|
||||
if (isPlusActive.value && autoReconnect.value) {
|
||||
term.value.write(`\r\n\x1b[91m${ tips },自动重连中...\x1b[0m \r\n`)
|
||||
connectIO()
|
||||
} else {
|
||||
term.value.write(`\r\n\x1b[91mError: ${ tips },请重新连接。([功能项->本地设置->快捷操作]中开启自动重连)\x1b[0m \r\n`)
|
||||
}
|
||||
} else {
|
||||
term.value.write(`\n${ tips } \n`)
|
||||
connectIO()
|
||||
}
|
||||
}
|
||||
|
||||
const createLocalTerminal = () => {
|
||||
let terminalInstance = new Terminal({
|
||||
rendererType: 'dom',
|
||||
@ -223,13 +248,6 @@ const createLocalTerminal = () => {
|
||||
fontFamily: 'Cascadia Code, Menlo, monospace',
|
||||
fontSize: fontSize.value,
|
||||
theme: theme.value
|
||||
// {
|
||||
// foreground: '#ECECEC',
|
||||
// background: '#000000', // 'transparent',
|
||||
// // cursor: 'help',
|
||||
// selection: '#ff9900',
|
||||
// lineHeight: 20
|
||||
// }
|
||||
})
|
||||
term.value = terminalInstance
|
||||
terminalInstance.open(terminalRef.value)
|
||||
@ -237,15 +255,20 @@ const createLocalTerminal = () => {
|
||||
terminalInstance.writeln('\x1b[1;32mAn experimental Web-SSH Terminal\x1b[0m.')
|
||||
terminalInstance.focus()
|
||||
onSelectionChange()
|
||||
onFindText()
|
||||
onWebLinks()
|
||||
onResize()
|
||||
terminal.value = terminalInstance
|
||||
}
|
||||
|
||||
const shellResize = () => {
|
||||
fitAddon.value.fit()
|
||||
let { rows, cols } = term.value
|
||||
socket.value?.emit('resize', { rows, cols })
|
||||
}
|
||||
const onResize = () => {
|
||||
fitAddon.value = new FitAddon()
|
||||
term.value.loadAddon(fitAddon.value)
|
||||
fitAddon.value.fit()
|
||||
let { rows, cols } = term.value
|
||||
socket.value.emit('resize', { rows, cols })
|
||||
window.addEventListener('resize', handleResize)
|
||||
}
|
||||
|
||||
@ -259,10 +282,7 @@ const handleResize = () => {
|
||||
temp[index] = item.style.display
|
||||
item.style.display = 'block'
|
||||
})
|
||||
fitAddon.value?.fit()
|
||||
let { rows, cols } = term.value
|
||||
socket.value?.emit('resize', { rows, cols })
|
||||
|
||||
shellResize()
|
||||
panes.forEach((item, index) => {
|
||||
item.style.display = temp[index]
|
||||
})
|
||||
@ -333,7 +353,13 @@ function extractLastCdPath(text) {
|
||||
|
||||
const onData = () => {
|
||||
term.value.onData((key) => {
|
||||
if (socketConnected.value === false) return
|
||||
if (!socket.value || !socketConnected.value) return
|
||||
|
||||
if ('\r' === key && curStatus.value === CONNECT_FAIL) {
|
||||
reconnectTerminal(false, '重新连接中...')
|
||||
return
|
||||
}
|
||||
|
||||
if (isLongPressCtrl.value || isLongPressAlt.value) {
|
||||
const keyCode = key.toUpperCase().charCodeAt(0)
|
||||
console.log('keyCode: ', keyCode)
|
||||
@ -354,12 +380,6 @@ const onData = () => {
|
||||
enterTimer.value = setTimeout(() => {
|
||||
if (enterTimer.value) clearTimeout(enterTimer.value)
|
||||
if (key === '\r') { // Enter
|
||||
if (curStatus.value === CONNECT_FAIL) { // 连接失败&&未正在连接,按回车可触发重连
|
||||
curStatus.value = CONNECTING
|
||||
term.value.write('\r\n连接中...\r\n')
|
||||
socket.value.emit('reconnect_terminal')
|
||||
return
|
||||
}
|
||||
if (curStatus.value === CONNECT_SUCCESS) {
|
||||
let cleanText = applyBackspace(filterAnsiSequences(terminalText.value))
|
||||
const lines = cleanText.split('\n')
|
||||
@ -369,7 +389,6 @@ const onData = () => {
|
||||
// 截取最后一个提示符后的内容('$'或'#'后的内容)
|
||||
const commandStartIndex = lastLine.lastIndexOf('#') + 1
|
||||
const commandText = lastLine.substring(commandStartIndex).trim()
|
||||
// console.log('Processed command: ', commandText)
|
||||
// eslint-disable-next-line
|
||||
const cdPath = extractLastCdPath(commandText)
|
||||
|
||||
|
@ -73,7 +73,7 @@
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="开启后同步键盘输入到所有会话"
|
||||
placement="top"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-switch
|
||||
v-model="isSyncAllSession"
|
||||
@ -89,7 +89,7 @@
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="SFTP文件传输"
|
||||
placement="top"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-switch
|
||||
v-model="showSftp"
|
||||
|
@ -10,7 +10,7 @@
|
||||
}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-show="!isAllConfssh" fixed="right" width="80px">
|
||||
<el-table-column fixed="right" width="80px">
|
||||
<template #default="{ row }">
|
||||
<div class="actios_btns">
|
||||
<el-button
|
||||
@ -71,9 +71,6 @@ const route = useRoute()
|
||||
|
||||
let showLinkTips = computed(() => !Boolean(terminalTabs.length))
|
||||
let hostList = computed(() => $store.hostList)
|
||||
let isAllConfssh = computed(() => {
|
||||
return hostList.value?.every(item => item.isConfig)
|
||||
})
|
||||
|
||||
function linkTerminal(hostInfo) {
|
||||
let targetHost = hostList.value.find(item => item.id === hostInfo.id)
|
||||
|
Loading…
x
Reference in New Issue
Block a user