mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-06-02 08:00:52 +08:00
feat: 新增代理服务
This commit is contained in:
@@ -21,6 +21,7 @@ module.exports = {
|
||||
aiConfigDBPath: path.join(process.cwd(),'app/db/ai-config.db'),
|
||||
chatHistoryDBPath: path.join(process.cwd(),'app/db/chat-history.db'),
|
||||
favoriteSftpDBPath: path.join(process.cwd(),'app/db/favorite-sftp.db'),
|
||||
proxyDBPath: path.join(process.cwd(),'app/db/proxy.db'),
|
||||
apiPrefix: '/api/v1',
|
||||
logConfig: {
|
||||
outDir: path.join(process.cwd(),'./app/db/logs'),
|
||||
|
||||
@@ -94,7 +94,8 @@ async function importHost({ res, request }) {
|
||||
} else {
|
||||
let extraFiels = {
|
||||
expired: null, expiredNotify: false, group: 'default', consoleUrl: '', remark: '',
|
||||
authType: 'privateKey', password: '', privateKey: '', credential: '', command: ''
|
||||
authType: 'privateKey', password: '', privateKey: '', credential: '', command: '',
|
||||
proxyType: '', jumpHosts: [], proxyServer: ''
|
||||
}
|
||||
newHostList = newHostList.map((item, index) => {
|
||||
item.port = Number(item.port) || 0
|
||||
|
||||
File diff suppressed because one or more lines are too long
117
server/app/controller/proxy.js
Normal file
117
server/app/controller/proxy.js
Normal file
@@ -0,0 +1,117 @@
|
||||
const { ProxyDB, HostListDB } = require('../utils/db-class')
|
||||
|
||||
const proxyDB = new ProxyDB().getInstance()
|
||||
const hostListDB = new HostListDB().getInstance()
|
||||
|
||||
const getProxyList = async ({ res }) => {
|
||||
try {
|
||||
let data = await proxyDB.findAsync({})
|
||||
|
||||
data = data.map(item => ({ ...item, id: item._id }))
|
||||
data?.sort((a, b) => new Date(b.createTime || 0) - new Date(a.createTime || 0))
|
||||
|
||||
res.success({ data })
|
||||
} catch (error) {
|
||||
res.fail({ data: false, msg: '获取代理列表失败' })
|
||||
}
|
||||
}
|
||||
|
||||
const addProxy = async ({ res, request }) => {
|
||||
try {
|
||||
let { body: { type, name, host, port, username, password } } = request
|
||||
|
||||
if (!type || !name || !host || !port) {
|
||||
return res.fail({ data: false, msg: '参数错误:类型、名称、主机、端口为必填项' })
|
||||
}
|
||||
|
||||
// 验证端口号
|
||||
const portNum = Number(port)
|
||||
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
|
||||
return res.fail({ data: false, msg: '端口号必须是1-65535之间的整数' })
|
||||
}
|
||||
|
||||
let record = {
|
||||
type,
|
||||
name,
|
||||
host,
|
||||
port: portNum,
|
||||
username: username || '',
|
||||
password: password || '',
|
||||
createTime: new Date().toISOString()
|
||||
}
|
||||
|
||||
await proxyDB.insertAsync(record)
|
||||
res.success({ data: '添加成功' })
|
||||
} catch (error) {
|
||||
res.fail({ data: false, msg: '添加代理失败' })
|
||||
}
|
||||
}
|
||||
|
||||
const updateProxy = async ({ res, request }) => {
|
||||
try {
|
||||
let { params: { id } } = request
|
||||
let { body: { type, name, host, port, username, password } } = request
|
||||
|
||||
if (!id || !type || !name || !host || !port) {
|
||||
return res.fail({ data: false, msg: '参数错误:ID、类型、名称、主机、端口为必填项' })
|
||||
}
|
||||
|
||||
// 验证端口号
|
||||
const portNum = Number(port)
|
||||
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
|
||||
return res.fail({ data: false, msg: '端口号必须是1-65535之间的整数' })
|
||||
}
|
||||
|
||||
let target = await proxyDB.findOneAsync({ _id: id })
|
||||
if (!target) {
|
||||
return res.fail({ data: false, msg: `代理ID ${ id } 不存在` })
|
||||
}
|
||||
|
||||
await proxyDB.updateAsync(
|
||||
{ _id: id },
|
||||
{
|
||||
$set: {
|
||||
type,
|
||||
name,
|
||||
host,
|
||||
port: portNum,
|
||||
username: username || '',
|
||||
password: password || '',
|
||||
updateTime: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
res.success({ data: '修改成功' })
|
||||
} catch (error) {
|
||||
res.fail({ data: false, msg: '修改代理失败' })
|
||||
}
|
||||
}
|
||||
|
||||
const removeProxy = async ({ res, request }) => {
|
||||
try {
|
||||
let { params: { id } } = request
|
||||
|
||||
if (!id) {
|
||||
return res.fail({ data: false, msg: '参数错误:缺少代理ID' })
|
||||
}
|
||||
|
||||
let target = await proxyDB.findOneAsync({ _id: id })
|
||||
if (!target) {
|
||||
return res.fail({ data: false, msg: `代理ID ${ id } 不存在` })
|
||||
}
|
||||
await proxyDB.removeAsync({ _id: id })
|
||||
// 删除代理后,将所有使用该代理的实例的proxyServer设置为空
|
||||
await hostListDB.updateAsync({ proxyServer: id }, { $set: { proxyServer: '' } })
|
||||
res.success({ data: '删除成功' })
|
||||
} catch (error) {
|
||||
res.fail({ data: false, msg: '删除代理失败' })
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getProxyList,
|
||||
addProxy,
|
||||
updateProxy,
|
||||
removeProxy
|
||||
}
|
||||
@@ -8,6 +8,7 @@ const { getScriptGroupList, addScriptGroup, removeScriptGroup, updateScriptGroup
|
||||
const { getOnekeyRecord, removeOnekeyRecord } = require('../controller/onekey')
|
||||
const { getLog, saveIpWhiteList, removeSomeLoginRecords } = require('../controller/log')
|
||||
const { getAIConfig, saveAIConfig, getAIModels, getChatHistory, saveChatHistory, removeChatHistory } = require('../controller/chat')
|
||||
const { getProxyList, addProxy, updateProxy, removeProxy } = require('../controller/proxy')
|
||||
|
||||
const ssh = [
|
||||
{
|
||||
@@ -305,4 +306,40 @@ const aiConfig = [
|
||||
controller: removeChatHistory
|
||||
}
|
||||
]
|
||||
module.exports = [].concat(ssh, host, user, notify, group, scripts, scriptGroup, onekey, log, aiConfig)
|
||||
|
||||
const proxy = [
|
||||
{
|
||||
method: 'get',
|
||||
path: '/proxy',
|
||||
controller: getProxyList
|
||||
},
|
||||
{
|
||||
method: 'post',
|
||||
path: '/proxy',
|
||||
controller: addProxy
|
||||
},
|
||||
{
|
||||
method: 'put',
|
||||
path: '/proxy/:id',
|
||||
controller: updateProxy
|
||||
},
|
||||
{
|
||||
method: 'delete',
|
||||
path: '/proxy/:id',
|
||||
controller: removeProxy
|
||||
}
|
||||
]
|
||||
|
||||
module.exports = [].concat(
|
||||
ssh,
|
||||
host,
|
||||
user,
|
||||
notify,
|
||||
group,
|
||||
scripts,
|
||||
scriptGroup,
|
||||
onekey,
|
||||
log,
|
||||
aiConfig,
|
||||
proxy
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ const http = require('http')
|
||||
const { httpPort } = require('./config')
|
||||
const middlewares = require('./middlewares')
|
||||
const wsTerminal = require('./socket/terminal')
|
||||
const wsTerminalSingleWindow = require('./socket/terminal-single-window')
|
||||
const wsSftpV2 = require('./socket/sftp-v2')
|
||||
const wsDocker = require('./socket/docker')
|
||||
const wsOnekey = require('./socket/onekey')
|
||||
@@ -25,7 +24,6 @@ const httpServer = () => {
|
||||
function serverHandler(app, server) {
|
||||
app.proxy = true // 用于nginx反代时获取真实客户端ip
|
||||
wsTerminal(server) // 终端
|
||||
wsTerminalSingleWindow(server) // 终端-单窗口
|
||||
wsSftpV2(server) // sftp-v2
|
||||
wsDocker(server) // docker
|
||||
wsOnekey(server) // 一键指令
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const rawPath = require('path')
|
||||
const { Server } = require('socket.io')
|
||||
const { Client: SSHClient } = require('ssh2')
|
||||
const { sendNoticeAsync } = require('../utils/notify')
|
||||
@@ -6,8 +5,7 @@ const { verifyAuthSync } = require('../utils/verify-auth')
|
||||
const { shellThrottle } = require('../utils/tools')
|
||||
const { isAllowedIp } = require('../utils/tools')
|
||||
const { HostListDB, OnekeyDB } = require('../utils/db-class')
|
||||
const { getConnectionOptions } = require('./terminal')
|
||||
const decryptAndExecuteAsync = require('../utils/decrypt-file')
|
||||
const { getConnectionOptions, handleProxyAndJumpHostConnection } = require('./terminal')
|
||||
const hostListDB = new HostListDB().getInstance()
|
||||
const onekeyDB = new OnekeyDB().getInstance()
|
||||
|
||||
@@ -126,27 +124,35 @@ module.exports = (httpServer) => {
|
||||
if (!targetHostsInfo.length) return socket.emit('create_fail', `未找到【${ hostIds }】服务器信息`)
|
||||
// 查找 hostInfo -> 并发执行
|
||||
socket.emit('ready')
|
||||
// 获取跳板机连接函数
|
||||
let { connectByJumpHosts = null } = (await decryptAndExecuteAsync(rawPath.join(__dirname, 'plus.js'))) || {}
|
||||
|
||||
let execPromise = targetHostsInfo.map((hostInfo, index) => {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async (resolve, reject) => {
|
||||
setTimeout(() => reject('执行超时'), timeout * 1000)
|
||||
let { host, port, jumpHosts } = hostInfo
|
||||
let { host, port } = hostInfo
|
||||
let curRes = { command, host, port, name: hostInfo.name, result: '', status: execStatusEnum.connecting, startDate: Date.now() + index }
|
||||
execResult.push(curRes)
|
||||
let jumpSshClients = []
|
||||
try {
|
||||
let { authInfo: targetConnectionOptions } = await getConnectionOptions(hostInfo._id)
|
||||
|
||||
// 处理跳板机连接
|
||||
let jumpHostResult = connectByJumpHosts && (await connectByJumpHosts(jumpHosts, targetConnectionOptions.host, targetConnectionOptions.port, socket))
|
||||
if (jumpHostResult) {
|
||||
targetConnectionOptions.sock = jumpHostResult.sock
|
||||
jumpSshClients = jumpHostResult.sshClients
|
||||
jumpSshClientsPool.push(jumpSshClients)
|
||||
consola.success('Onekey跳板机连接成功')
|
||||
// 使用通用的代理和跳板机连接处理函数
|
||||
try {
|
||||
const result = await handleProxyAndJumpHostConnection({
|
||||
hostInfo,
|
||||
targetConnectionOptions,
|
||||
socket: null, // Onekey不需要发送terminal_print_info事件
|
||||
logPrefix: 'Onekey '
|
||||
})
|
||||
jumpSshClients = result.jumpSshClients
|
||||
if (jumpSshClients.length > 0) {
|
||||
jumpSshClientsPool.push(jumpSshClients)
|
||||
}
|
||||
} catch (proxyError) {
|
||||
curRes.status = execStatusEnum.connectFail
|
||||
curRes.result += `代理连接失败: ${ proxyError.message }`
|
||||
socket.emit('output', execResult)
|
||||
resolve(curRes)
|
||||
return
|
||||
}
|
||||
|
||||
consola.info('准备连接终端执行一次性指令:', host)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,8 +6,7 @@ const { sftpCacheDir } = require('../config')
|
||||
const { verifyAuthSync } = require('../utils/verify-auth')
|
||||
const { isAllowedIp } = require('../utils/tools')
|
||||
const { HostListDB, FavoriteSftpDB } = require('../utils/db-class')
|
||||
const { getConnectionOptions } = require('./terminal')
|
||||
const decryptAndExecuteAsync = require('../utils/decrypt-file')
|
||||
const { getConnectionOptions, handleProxyAndJumpHostConnection } = require('./terminal')
|
||||
const hostListDB = new HostListDB().getInstance()
|
||||
const favoriteSftpDB = new FavoriteSftpDB().getInstance()
|
||||
const { Client: SSHClient } = require('ssh2')
|
||||
@@ -1355,15 +1354,21 @@ module.exports = (httpServer) => {
|
||||
const targetHostInfo = await hostListDB.findOneAsync({ _id: hostId })
|
||||
if (!targetHostInfo) throw new Error(`Host with ID ${ hostId } not found`)
|
||||
|
||||
let { connectByJumpHosts = null } = (await decryptAndExecuteAsync(rawPath.join(__dirname, 'plus.js'))) || {}
|
||||
let { authType, host, port, username, jumpHosts } = targetHostInfo
|
||||
|
||||
let { authType, host, port, username } = targetHostInfo
|
||||
let { authInfo: targetConnectionOptions } = await getConnectionOptions(hostId)
|
||||
let jumpHostResult = connectByJumpHosts && (await connectByJumpHosts(jumpHosts, targetConnectionOptions.host, targetConnectionOptions.port, socket))
|
||||
if (jumpHostResult) {
|
||||
targetConnectionOptions.sock = jumpHostResult.sock
|
||||
jumpSshClients = jumpHostResult.sshClients
|
||||
consola.success('sftp-v2 跳板机连接成功')
|
||||
|
||||
// 使用通用的代理和跳板机连接处理函数
|
||||
try {
|
||||
const result = await handleProxyAndJumpHostConnection({
|
||||
hostInfo: targetHostInfo,
|
||||
targetConnectionOptions,
|
||||
socket: null, // SFTP不需要发送terminal_print_info事件
|
||||
logPrefix: 'SFTP '
|
||||
})
|
||||
jumpSshClients = result.jumpSshClients
|
||||
} catch (proxyError) {
|
||||
socket.emit('connect_fail', `代理连接失败: ${ proxyError.message }`)
|
||||
return
|
||||
}
|
||||
|
||||
consola.info('准备连接sftp-v2 面板:', host)
|
||||
@@ -1406,17 +1411,17 @@ module.exports = (httpServer) => {
|
||||
})
|
||||
|
||||
// 添加未处理异常捕获
|
||||
sftpClient.client.on('timeout', () => {
|
||||
consola.warn('SSH连接超时')
|
||||
try {
|
||||
socket.emit('shell_connection_error', {
|
||||
message: 'SSH连接超时',
|
||||
code: 'CONNECTION_TIMEOUT'
|
||||
})
|
||||
} catch (emitError) {
|
||||
consola.error('发送超时事件失败:', emitError.message)
|
||||
}
|
||||
})
|
||||
// sftpClient.client.on('timeout', () => {
|
||||
// consola.warn('SSH连接超时')
|
||||
// try {
|
||||
// socket.emit('shell_connection_error', {
|
||||
// message: 'SSH连接超时',
|
||||
// code: 'CONNECTION_TIMEOUT'
|
||||
// })
|
||||
// } catch (emitError) {
|
||||
// consola.error('发送超时事件失败:', emitError.message)
|
||||
// }
|
||||
// })
|
||||
|
||||
sftpClient.client.on('keyboard-interactive', function (name, instructions, instructionsLang, prompts, finish) {
|
||||
finish([targetConnectionOptions[authType]])
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
const path = require('path')
|
||||
const { Server } = require('socket.io')
|
||||
const decryptAndExecuteAsync = require('../utils/decrypt-file')
|
||||
|
||||
async function createServerIo(serverIo) {
|
||||
let { createSingleWindowServerIo = null } = (await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))) || {}
|
||||
if (!createSingleWindowServerIo) {
|
||||
consola.info('单窗口模式功能未解锁')
|
||||
return
|
||||
}
|
||||
createSingleWindowServerIo(serverIo)
|
||||
}
|
||||
|
||||
module.exports = (httpServer) => {
|
||||
const serverIo = new Server(httpServer, {
|
||||
path: '/terminal-single-window',
|
||||
cors: {
|
||||
origin: '*'
|
||||
}
|
||||
})
|
||||
createServerIo(serverIo)
|
||||
}
|
||||
@@ -1,233 +1,347 @@
|
||||
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 { 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, reject) => {
|
||||
// 检查SSH客户端连接状态
|
||||
if (!targetSSHClient || !targetSSHClient._sock || !targetSSHClient._sock.writable) {
|
||||
const errorMsg = 'SSH客户端连接已断开,无法创建交互式终端'
|
||||
consola.error(errorMsg)
|
||||
socket.emit('terminal_connect_fail', errorMsg)
|
||||
return reject(new Error(errorMsg))
|
||||
}
|
||||
|
||||
try {
|
||||
targetSSHClient.shell({ term: 'xterm-color' }, (err, stream) => {
|
||||
if (err) {
|
||||
consola.error('创建交互式终端失败:', err.message)
|
||||
socket.emit('terminal_connect_fail', err.message)
|
||||
return reject(err)
|
||||
}
|
||||
|
||||
resolve(stream)
|
||||
|
||||
stream
|
||||
.on('data', (data) => {
|
||||
socket.emit('output', data.toString())
|
||||
})
|
||||
.on('close', () => {
|
||||
consola.info('交互终端已关闭')
|
||||
targetSSHClient.end()
|
||||
})
|
||||
.on('error', (streamErr) => {
|
||||
consola.error('终端流错误:', streamErr.message)
|
||||
socket.emit('terminal_connect_fail', streamErr.message)
|
||||
})
|
||||
|
||||
socket.emit('terminal_connect_shell_success') // 已连接终端,web端可以执行指令了
|
||||
})
|
||||
} catch (shellError) {
|
||||
consola.error('调用shell方法失败:', shellError.message)
|
||||
socket.emit('terminal_connect_fail', shellError.message)
|
||||
reject(shellError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function createTerminal(hostId, socket, targetSSHClient, isInteractiveShell = true) {
|
||||
consola.info(`准备创建${ isInteractiveShell ? '交互式' : '非交互式' }终端:${ hostId }`)
|
||||
return new Promise(async (resolve) => {
|
||||
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 {
|
||||
let { authInfo: targetConnectionOptions } = await getConnectionOptions(hostId)
|
||||
let jumpHostResult = connectByJumpHosts && (await connectByJumpHosts(jumpHosts, targetConnectionOptions.host, targetConnectionOptions.port, socket))
|
||||
let jumpSshClients = []
|
||||
if (jumpHostResult) {
|
||||
targetConnectionOptions.sock = jumpHostResult.sock
|
||||
jumpSshClients = jumpHostResult.sshClients
|
||||
}
|
||||
|
||||
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 })
|
||||
|
||||
let closeNoticeFlag = false // 避免重复发送通知
|
||||
targetSSHClient
|
||||
.on('ready', async () => {
|
||||
consola.success('终端连接成功:', host)
|
||||
if (isInteractiveShell) {
|
||||
sendNoticeAsync('host_login', '终端登录', `别名: ${ name } \n IP:${ host } \n 端口:${ port } \n 状态: 登录成功`)
|
||||
socket.emit('terminal_print_info', `终端连接成功: ${ name } - ${ host }`)
|
||||
socket.emit('terminal_connect_success', `终端连接成功:${ host }`)
|
||||
|
||||
try {
|
||||
let stream = await createInteractiveShell(socket, targetSSHClient)
|
||||
resolve({ stream, jumpSshClients })
|
||||
} catch (shellError) {
|
||||
consola.error('创建交互式终端失败:', host, shellError.message)
|
||||
// 连接已经成功但创建shell失败,需要清理连接
|
||||
targetSSHClient.end()
|
||||
jumpSshClients?.forEach(sshClient => sshClient && sshClient.end())
|
||||
}
|
||||
} else {
|
||||
resolve({ jumpSshClients })
|
||||
}
|
||||
})
|
||||
.on('close', (err) => {
|
||||
if (closeNoticeFlag) return closeNoticeFlag = false
|
||||
const closeReason = err ? '发生错误导致连接断开' : '正常断开连接'
|
||||
consola.info(`终端连接断开(${ closeReason }): ${ host }`)
|
||||
socket.emit('terminal_connect_close', { reason: closeReason })
|
||||
})
|
||||
.on('error', (err) => {
|
||||
closeNoticeFlag = true
|
||||
sendNoticeAsync('host_login', '终端登录', `别名: ${ name } \n IP:${ host } \n 端口:${ port } \n 状态: 登录失败`)
|
||||
consola.error('连接终端失败:', host, err.message)
|
||||
socket.emit('terminal_connect_fail', err.message)
|
||||
})
|
||||
.on('keyboard-interactive', function (name, instructions, instructionsLang, prompts, finish) {
|
||||
finish([targetConnectionOptions[authType]])
|
||||
})
|
||||
.connect({
|
||||
tryKeyboard: true,
|
||||
...targetConnectionOptions
|
||||
// debug: (info) => console.log(info)
|
||||
})
|
||||
} catch (err) {
|
||||
consola.error('创建终端失败: ', host, err.message)
|
||||
socket.emit('terminal_create_fail', err.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function createServerIo(serverIo) {
|
||||
let connectionCount = 0
|
||||
|
||||
serverIo.on('connection', (socket) => {
|
||||
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地址不在白名单中')
|
||||
socket.disconnect()
|
||||
return
|
||||
}
|
||||
consola.success('terminal websocket 已连接')
|
||||
let targetSSHClient = null
|
||||
let jumpSshClients = []
|
||||
socket.on('ws_terminal', async ({ hostId, token }) => {
|
||||
try {
|
||||
const { code } = await verifyAuthSync(token, requestIP)
|
||||
if (code !== 1) {
|
||||
socket.emit('token_verify_fail')
|
||||
socket.disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
targetSSHClient = new SSHClient()
|
||||
let result = await createTerminal(hostId, socket, targetSSHClient, true)
|
||||
|
||||
// 如果创建终端失败,result可能为undefined
|
||||
if (!result) {
|
||||
consola.error('创建终端失败,未返回结果')
|
||||
return
|
||||
}
|
||||
|
||||
let { stream = null, jumpSshClients: jumpSshClientsFromCreate } = result
|
||||
jumpSshClients = jumpSshClientsFromCreate || []
|
||||
|
||||
const listenerInput = (key) => {
|
||||
if (!targetSSHClient || !targetSSHClient._sock || !targetSSHClient._sock.writable) {
|
||||
consola.info('终端连接已关闭,禁止输入')
|
||||
return
|
||||
}
|
||||
stream && stream.write(key)
|
||||
}
|
||||
|
||||
const resizeShell = ({ rows, cols }) => {
|
||||
stream && stream.setWindow(rows, cols)
|
||||
}
|
||||
|
||||
socket.on('input', listenerInput)
|
||||
socket.on('resize', resizeShell)
|
||||
} catch (error) {
|
||||
consola.error('ws_terminal事件处理失败:', error.message)
|
||||
socket.emit('terminal_connect_fail', `连接失败: ${ error.message }`)
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('get_ping', async (ip) => {
|
||||
try {
|
||||
socket.emit('ping_data', await ping(ip, 2500))
|
||||
} catch (error) {
|
||||
socket.emit('ping_data', { success: false, msg: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('disconnect', (reason) => {
|
||||
connectionCount--
|
||||
targetSSHClient && targetSSHClient.end()
|
||||
jumpSshClients?.forEach(sshClient => sshClient && sshClient.end())
|
||||
targetSSHClient = null
|
||||
jumpSshClients = null
|
||||
consola.info(`终端socket连接断开: ${ reason } - 当前连接数: ${ connectionCount }`)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = (httpServer) => {
|
||||
const serverIo = new Server(httpServer, {
|
||||
path: '/terminal',
|
||||
cors: {
|
||||
origin: '*'
|
||||
}
|
||||
})
|
||||
createServerIo(serverIo)
|
||||
}
|
||||
|
||||
module.exports.getConnectionOptions = getConnectionOptions
|
||||
module.exports.createTerminal = createTerminal
|
||||
module.exports.createServerIo = createServerIo
|
||||
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 { AESDecryptAsync } = require('../utils/encrypt')
|
||||
const { HostListDB, CredentialsDB, ProxyDB } = require('../utils/db-class')
|
||||
const decryptAndExecuteAsync = require('../utils/decrypt-file')
|
||||
const hostListDB = new HostListDB().getInstance()
|
||||
const credentialsDB = new CredentialsDB().getInstance()
|
||||
const proxyDB = new ProxyDB().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, reject) => {
|
||||
// 检查SSH客户端连接状态
|
||||
if (!targetSSHClient || !targetSSHClient._sock || !targetSSHClient._sock.writable) {
|
||||
const errorMsg = 'SSH客户端连接已断开,无法创建交互式终端'
|
||||
consola.error(errorMsg)
|
||||
socket.emit('terminal_connect_fail', errorMsg)
|
||||
return reject(new Error(errorMsg))
|
||||
}
|
||||
|
||||
try {
|
||||
targetSSHClient.shell({ term: 'xterm-color' }, (err, stream) => {
|
||||
if (err) {
|
||||
consola.error('创建交互式终端失败:', err.message)
|
||||
socket.emit('terminal_connect_fail', err.message)
|
||||
return reject(err)
|
||||
}
|
||||
|
||||
resolve(stream)
|
||||
|
||||
stream
|
||||
.on('data', (data) => {
|
||||
socket.emit('output', data.toString())
|
||||
})
|
||||
.on('close', () => {
|
||||
consola.info('交互终端已关闭')
|
||||
targetSSHClient.end()
|
||||
})
|
||||
.on('error', (streamErr) => {
|
||||
consola.error('终端流错误:', streamErr.message)
|
||||
socket.emit('terminal_connect_fail', streamErr.message)
|
||||
})
|
||||
|
||||
socket.emit('terminal_connect_shell_success') // 已连接终端,web端可以执行指令了
|
||||
})
|
||||
} catch (shellError) {
|
||||
consola.error('调用shell方法失败:', shellError.message)
|
||||
socket.emit('terminal_connect_fail', shellError.message)
|
||||
reject(shellError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取代理配置信息
|
||||
async function getProxyConfig(proxyId) {
|
||||
if (!proxyId) return null
|
||||
try {
|
||||
const proxyInfo = await proxyDB.findOneAsync({ _id: proxyId })
|
||||
if (!proxyInfo) {
|
||||
throw new Error(`代理配置 ID ${ proxyId } 未找到`)
|
||||
}
|
||||
|
||||
return {
|
||||
id: proxyInfo._id,
|
||||
name: proxyInfo.name,
|
||||
type: proxyInfo.type, // 'socks5' 或 'http'
|
||||
host: proxyInfo.host,
|
||||
port: proxyInfo.port,
|
||||
username: proxyInfo.username || '',
|
||||
password: proxyInfo.password || ''
|
||||
}
|
||||
} catch (error) {
|
||||
consola.error('获取代理配置失败:', error.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 通用的代理和跳板机连接处理函数
|
||||
async function handleProxyAndJumpHostConnection(options) {
|
||||
const {
|
||||
hostInfo,
|
||||
targetConnectionOptions,
|
||||
socket,
|
||||
logPrefix = ''
|
||||
} = options
|
||||
|
||||
const { proxyType, proxyServer, jumpHosts, host } = hostInfo
|
||||
let jumpSshClients = []
|
||||
|
||||
try {
|
||||
// 代理连接
|
||||
if (proxyType === 'proxyServer' && proxyServer) {
|
||||
const proxyConfig = await getProxyConfig(proxyServer)
|
||||
if (proxyConfig) {
|
||||
const logMsg = `${ logPrefix }使用代理服务器: ${ proxyConfig.name } (${ proxyConfig.type.toUpperCase() }) - ${ proxyConfig.host }:${ proxyConfig.port }`
|
||||
consola.info(logMsg)
|
||||
|
||||
// 向前端发送代理信息(如果socket存在且有对应方法)
|
||||
if (socket && socket.emit) {
|
||||
if (typeof socket.emit === 'function') {
|
||||
try {
|
||||
socket.emit('terminal_print_info', `使用代理服务器: ${ proxyConfig.name } (${ proxyConfig.type.toUpperCase() }) - ${ proxyConfig.host }:${ proxyConfig.port }`)
|
||||
} catch (emitError) {
|
||||
// 忽略emit错误,因为不同socket可能有不同的事件
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let proxySocket
|
||||
if (proxyConfig.type === 'socks5') {
|
||||
const { createSocks5Connection = null } = (await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))) || {}
|
||||
if (!createSocks5Connection) throw new Error('Plus功能解锁失败: createSocks5Connection')
|
||||
proxySocket = await createSocks5Connection(proxyConfig, targetConnectionOptions.host, targetConnectionOptions.port)
|
||||
} else if (proxyConfig.type === 'http') {
|
||||
const { createHttpConnection = null } = (await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))) || {}
|
||||
if (!createHttpConnection) throw new Error('Plus功能解锁失败: createHttpConnection')
|
||||
proxySocket = await createHttpConnection(proxyConfig, targetConnectionOptions.host, targetConnectionOptions.port)
|
||||
} else {
|
||||
throw new Error(`不支持的代理类型: ${ proxyConfig.type }`)
|
||||
}
|
||||
|
||||
targetConnectionOptions.sock = proxySocket
|
||||
consola.success(`${ logPrefix }代理连接建立成功: ${ host }`)
|
||||
|
||||
// 向前端发送成功信息
|
||||
if (socket && socket.emit && typeof socket.emit === 'function') {
|
||||
try {
|
||||
socket.emit('terminal_print_info', '代理连接建立成功,准备通过代理连接目标服务器')
|
||||
} catch (emitError) {
|
||||
// 忽略emit错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 跳板机连接
|
||||
else if (proxyType === 'jumpHosts' && Array.isArray(jumpHosts) && jumpHosts.length > 0) {
|
||||
const { connectByJumpHosts = null } = (await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))) || {}
|
||||
if (!connectByJumpHosts) throw new Error('Plus功能解锁失败: connectByJumpHosts')
|
||||
const jumpHostResult = await connectByJumpHosts(jumpHosts, targetConnectionOptions.host, targetConnectionOptions.port, socket)
|
||||
if (jumpHostResult) {
|
||||
targetConnectionOptions.sock = jumpHostResult.sock
|
||||
jumpSshClients = jumpHostResult.sshClients
|
||||
consola.success(`${ logPrefix }跳板机连接成功`)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
targetConnectionOptions,
|
||||
jumpSshClients
|
||||
}
|
||||
} catch (error) {
|
||||
consola.error(`${ logPrefix }连接失败:`, error.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function createTerminal(hostId, socket, targetSSHClient, isInteractiveShell = true) {
|
||||
consola.info(`准备创建${ isInteractiveShell ? '交互式' : '非交互式' }终端:${ hostId }`)
|
||||
return new Promise(async (resolve) => {
|
||||
const targetHostInfo = await hostListDB.findOneAsync({ _id: hostId })
|
||||
if (!targetHostInfo) return socket.emit('create_fail', `查找hostId【${ hostId }】凭证信息失败`)
|
||||
let { authType, host, port, username, name } = targetHostInfo
|
||||
try {
|
||||
let { authInfo: targetConnectionOptions } = await getConnectionOptions(hostId)
|
||||
|
||||
// 使用通用的代理和跳板机连接处理函数
|
||||
let jumpSshClients = []
|
||||
try {
|
||||
const result = await handleProxyAndJumpHostConnection({
|
||||
hostInfo: targetHostInfo,
|
||||
targetConnectionOptions,
|
||||
socket,
|
||||
logPrefix: 'Terminal '
|
||||
})
|
||||
jumpSshClients = result.jumpSshClients
|
||||
} catch (proxyError) {
|
||||
socket.emit('terminal_connect_fail', `代理连接失败: ${ proxyError.message }`)
|
||||
return
|
||||
}
|
||||
|
||||
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 })
|
||||
|
||||
let closeNoticeFlag = false // 避免重复发送通知
|
||||
targetSSHClient
|
||||
.on('ready', async () => {
|
||||
consola.success('终端连接成功:', host)
|
||||
if (isInteractiveShell) {
|
||||
sendNoticeAsync('host_login', '终端登录', `别名: ${ name } \n IP:${ host } \n 端口:${ port } \n 状态: 登录成功`)
|
||||
socket.emit('terminal_print_info', `终端连接成功: ${ name } - ${ host }`)
|
||||
socket.emit('terminal_connect_success', `终端连接成功:${ host }`)
|
||||
|
||||
try {
|
||||
let stream = await createInteractiveShell(socket, targetSSHClient)
|
||||
resolve({ stream, jumpSshClients })
|
||||
} catch (shellError) {
|
||||
consola.error('创建交互式终端失败:', host, shellError.message)
|
||||
// 连接已经成功但创建shell失败,需要清理连接
|
||||
targetSSHClient.end()
|
||||
jumpSshClients?.forEach(sshClient => sshClient && sshClient.end())
|
||||
}
|
||||
} else {
|
||||
resolve({ jumpSshClients })
|
||||
}
|
||||
})
|
||||
.on('close', (err) => {
|
||||
if (closeNoticeFlag) return closeNoticeFlag = false
|
||||
const closeReason = err ? '发生错误导致连接断开' : '正常断开连接'
|
||||
consola.info(`终端连接断开(${ closeReason }): ${ host }`)
|
||||
socket.emit('terminal_connect_close', { reason: closeReason })
|
||||
})
|
||||
.on('error', (err) => {
|
||||
closeNoticeFlag = true
|
||||
sendNoticeAsync('host_login', '终端登录', `别名: ${ name } \n IP:${ host } \n 端口:${ port } \n 状态: 登录失败`)
|
||||
consola.error('连接终端失败:', host, err.message)
|
||||
socket.emit('terminal_connect_fail', err.message)
|
||||
})
|
||||
.on('keyboard-interactive', function (name, instructions, instructionsLang, prompts, finish) {
|
||||
finish([targetConnectionOptions[authType]])
|
||||
})
|
||||
.connect({
|
||||
tryKeyboard: true,
|
||||
...targetConnectionOptions
|
||||
// debug: (info) => console.log(info)
|
||||
})
|
||||
} catch (err) {
|
||||
consola.error('创建终端失败: ', host, err.message)
|
||||
socket.emit('terminal_create_fail', err.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function createServerIo(serverIo) {
|
||||
let connectionCount = 0
|
||||
|
||||
serverIo.on('connection', (socket) => {
|
||||
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地址不在白名单中')
|
||||
socket.disconnect()
|
||||
return
|
||||
}
|
||||
consola.success('terminal websocket 已连接')
|
||||
let targetSSHClient = null
|
||||
let jumpSshClients = []
|
||||
socket.on('ws_terminal', async ({ hostId, token }) => {
|
||||
try {
|
||||
const { code } = await verifyAuthSync(token, requestIP)
|
||||
if (code !== 1) {
|
||||
socket.emit('token_verify_fail')
|
||||
socket.disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
targetSSHClient = new SSHClient()
|
||||
let result = await createTerminal(hostId, socket, targetSSHClient, true)
|
||||
|
||||
// 如果创建终端失败,result可能为undefined
|
||||
if (!result) {
|
||||
consola.error('创建终端失败,未返回结果')
|
||||
return
|
||||
}
|
||||
|
||||
let { stream = null, jumpSshClients: jumpSshClientsFromCreate } = result
|
||||
jumpSshClients = jumpSshClientsFromCreate || []
|
||||
|
||||
const listenerInput = (key) => {
|
||||
if (!targetSSHClient || !targetSSHClient._sock || !targetSSHClient._sock.writable) {
|
||||
consola.info('终端连接已关闭,禁止输入')
|
||||
return
|
||||
}
|
||||
stream && stream.write(key)
|
||||
}
|
||||
|
||||
const resizeShell = ({ rows, cols }) => {
|
||||
stream && stream.setWindow(rows, cols)
|
||||
}
|
||||
|
||||
socket.on('input', listenerInput)
|
||||
socket.on('resize', resizeShell)
|
||||
} catch (error) {
|
||||
consola.error('ws_terminal事件处理失败:', error.message)
|
||||
socket.emit('terminal_connect_fail', `连接失败: ${ error.message }`)
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('get_ping', async (ip) => {
|
||||
try {
|
||||
socket.emit('ping_data', await ping(ip, 2500))
|
||||
} catch (error) {
|
||||
socket.emit('ping_data', { success: false, msg: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('disconnect', (reason) => {
|
||||
connectionCount--
|
||||
targetSSHClient && targetSSHClient.end()
|
||||
jumpSshClients?.forEach(sshClient => sshClient && sshClient.end())
|
||||
targetSSHClient = null
|
||||
jumpSshClients = null
|
||||
consola.info(`终端socket连接断开: ${ reason } - 当前连接数: ${ connectionCount }`)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = (httpServer) => {
|
||||
const serverIo = new Server(httpServer, {
|
||||
path: '/terminal',
|
||||
cors: {
|
||||
origin: '*'
|
||||
}
|
||||
})
|
||||
createServerIo(serverIo)
|
||||
}
|
||||
|
||||
module.exports.getConnectionOptions = getConnectionOptions
|
||||
module.exports.createTerminal = createTerminal
|
||||
module.exports.createServerIo = createServerIo
|
||||
module.exports.getProxyConfig = getProxyConfig
|
||||
module.exports.handleProxyAndJumpHostConnection = handleProxyAndJumpHostConnection
|
||||
|
||||
@@ -13,7 +13,8 @@ const {
|
||||
plusDBPath,
|
||||
aiConfigDBPath,
|
||||
chatHistoryDBPath,
|
||||
favoriteSftpDBPath
|
||||
favoriteSftpDBPath,
|
||||
proxyDBPath
|
||||
} = require('../config')
|
||||
|
||||
module.exports.KeyDB = class KeyDB {
|
||||
@@ -178,3 +179,14 @@ module.exports.FavoriteSftpDB = class FavoriteSftpDB {
|
||||
return FavoriteSftpDB.instance
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.ProxyDB = class ProxyDB {
|
||||
constructor() {
|
||||
if (!ProxyDB.instance) {
|
||||
ProxyDB.instance = new Datastore({ filename: proxyDBPath, autoload: true })
|
||||
}
|
||||
}
|
||||
getInstance() {
|
||||
return ProxyDB.instance
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
U2FsdGVkX188iPUyO1f2xahkUzOdBH0ifn8ZV/KNKGkTSDSDEhT8qEioUui8+nM/p4NQJ2LAbVqK4fmyuG1rUy8VHbFBJGxEWCa5cgFemgxRWpZE2K9lWr1sopjfzsyN1hGfdi2oWZE8/x1MMw6Rc1K6guFYBuj4LGuGS/yp8wUpGe4Ifb5d/W81Y/YHqoH/WRUJc8V3EbnlHgweY7Yx6NcuRMHaqMVdL7P/1do4Xig4N5zqHdPmZE6gp2tAKh8WwpzpLgTqlXEEUFQk0KL07lYf7PNVKudsD2G5s1VYrhVhvR+1jeSh7AwLa4pABXg8bUo53SC4CCgSC57GLqldmnqJP8Kqr8ULSEdFVL0uLS4ogTALgXrrTvzr3hXAccIQSRLihD2BDK0MuDNcLpOXbYI6KQ6L4Z1/cT8VOteV7OMtJMC3EZDUroOwYS1oeToIQoZFHYKIyqlyThLEkb9zZBRDBvLisirGGvTh52bzw1BKE5IZznAe1A5N0n9U2eawqIUDJMlIRE4LEPuSzkNPDV6v3IiNsb3c7JO7t06WQMIs4Nene2v0SDwYbbZgBhfgEu+GkKwXh16w1txX/juJ09gfTj6vZiJNRk4bOGZJsPWujQhOjncjbYRq7dhZA1/0gjOQppX312g/yIznIYFKkl5LF868hptbFu6WgTbclqwLiUKX0D+Oe25mcMIDXdhjWeZZoEIXg5FFv1PaE5Qrj35Uowo4MzP6aVYw2zn9WlxGzbHPtAFCTKChcOuSV7RYuRETR8OIHyNcOlNBSbJ5KlALb2XS5XEtluhEF/G2iljXSvlACNHLz9EZ3E4Q9XubvMW5YBecdcW3XHipbusKZCA542BKxadQYQGGWryZ/wQ=
|
||||
U2FsdGVkX19ypJFyeFxNF22xekv1FOR0XT1ofsw1MQ8fs/xIpP4rByqxDo2e7/E4w3sSaVpT3IUt5jH0alt4GeryiM3wfedmo+4eBiw2qTTBW0tQHCbh2tIIxJWPv3NdaMCv0fEzJNJf2CrhgrRSv9RpUpDYYojpg50wnKjL2Fiw37mGw9uqHowmgqoyxO14Otjbh9Sf2Y9DqoYr4SF/uvxPGeFpBO3op/+8gnqgN3r0mmF8DLn73m5a7NANNe6IuHXCwfu3DY9q9xgI6OEp2ecW/rxXijGRllMmVkdSKeG0xzSeV7q1PF+qpvEo9xz8FdVBOW4Te6G/hDpwA3WI0cwKY5nOs+aY4J8KNmtpsQnUMbjm9w5OjCrKOBzyQ6JIYbT6b7vP4jyF6FLOR0DJJ3JRP1OGbDLZhoWkXtNB7Mwa85KegKVx3Hg2eEsEug93jQrymm++YtFvoEbdAD5AeA8NX/qJRVqloOikv4d/mCy1BJaAQE4tAHNIldwMCvTarUKrYSxjdzO0sZmRuE4PhwjIGIN5dK8/GYR9vFfnGuNhM+w0gRJr1VDK+/6XnMp0PmQMx7MYO4UE2A4zulIsxhWxYRhzvmePPWf5rOIPnSl8dYx/otqgs6LY+FDkMWa99itdFFhCZxjapUJ2917ofYuAH8Y76QliRVgZ9HOPgpJOr/A02dt3c+Kp/p9cu6vAuGJ29l9JDEr1d35+/hRvvR6SEWYoalFEjkyEl4Y1cuxyNasAxkpxFlN9/ROl1D3qDk1L/lci4YC1aXlyW8jLyNthIXFMHWTiawA7xEV0iFiSMjDkpIpv+1/V8T+b7djl13/ALfgw/Zw7rNDKNqj8vaYLNO2T7epyFeESWE8EY9Q=
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "3.2.0",
|
||||
"version": "3.3.0",
|
||||
"description": "easynode-server",
|
||||
"bin": "./bin/www",
|
||||
"scripts": {
|
||||
@@ -48,6 +48,7 @@
|
||||
"qrcode": "^1.5.4",
|
||||
"socket.io": "^4.7.5",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"socks": "^2.8.6",
|
||||
"speakeasy": "^2.0.0",
|
||||
"ssh2": "^1.15.0",
|
||||
"ssh2-sftp-client": "^10.0.3"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "3.2.0",
|
||||
"version": "3.3.0",
|
||||
"description": "easynode-web",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -168,5 +168,17 @@ export default {
|
||||
},
|
||||
removeSomeLoginRecords() {
|
||||
return axios({ url: '/remove-some-login-records', method: 'delete' })
|
||||
},
|
||||
getProxyList() {
|
||||
return axios({ url: '/proxy', method: 'get' })
|
||||
},
|
||||
addProxy(data) {
|
||||
return axios({ url: '/proxy', method: 'post', data })
|
||||
},
|
||||
updateProxy(id, data) {
|
||||
return axios({ url: `/proxy/${ id }`, method: 'put', data })
|
||||
},
|
||||
removeProxy(id) {
|
||||
return axios({ url: `/proxy/${ id }`, method: 'delete' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,8 +94,9 @@ const basicFeatures = [
|
||||
// Plus版专属功能列表
|
||||
const plusFeatures = [
|
||||
'AI Chat对话组件',
|
||||
'服务器跳板机功能,支持任意数量服务器的连续跳板',
|
||||
'批量修改实例配置(优化版)',
|
||||
'服务器代理&跳板机功能',
|
||||
'终端单窗口模式',
|
||||
'批量修改实例配置',
|
||||
'脚本库批量导出导入',
|
||||
'脚本库分组与终端功能栏支持',
|
||||
'终端功能栏docker容器管理',
|
||||
|
||||
@@ -11,6 +11,7 @@ const useStore = defineStore('global', {
|
||||
sshList: [],
|
||||
scriptList: [],
|
||||
scriptGroupList: [],
|
||||
proxyList: [],
|
||||
localScriptList: [],
|
||||
user: localStorage.getItem('user') || null,
|
||||
token: localStorage.getItem('token') || sessionStorage.getItem('token') || null,
|
||||
@@ -76,6 +77,7 @@ const useStore = defineStore('global', {
|
||||
await this.getScriptList()
|
||||
await this.getScriptGroupList()
|
||||
await this.getPlusInfo()
|
||||
await this.getProxyList()
|
||||
this.getAIConfig()
|
||||
this.getChatHistory()
|
||||
},
|
||||
@@ -117,6 +119,10 @@ const useStore = defineStore('global', {
|
||||
const { data: localScriptList } = await $api.getLocalScriptList()
|
||||
this.$patch({ localScriptList })
|
||||
},
|
||||
async getProxyList() {
|
||||
const { data: proxyList } = await $api.getProxyList()
|
||||
this.$patch({ proxyList })
|
||||
},
|
||||
async getPlusInfo() {
|
||||
const { data: plusInfo = {} } = await $api.getPlusInfo()
|
||||
if (plusInfo?.expiryDate) {
|
||||
|
||||
@@ -440,25 +440,6 @@ const handleRemoveAll = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
onActivated(async () => {
|
||||
await nextTick()
|
||||
const { hostIds, execClientInstallScript } = route.query
|
||||
if (!hostIds) return
|
||||
if (execClientInstallScript === 'true') {
|
||||
let clientInstallScript = 'curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash\n'
|
||||
console.log(hostIds.split(','))
|
||||
createExecShell(hostIds.split(','), clientInstallScript, 300)
|
||||
// $messageBox.confirm(`准备安装客户端服务监控应用:${ host }`, 'Warning', {
|
||||
// confirmButtonText: '确定',
|
||||
// cancelButtonText: '取消',
|
||||
// type: 'warning'
|
||||
// })
|
||||
// .then(async () => {
|
||||
// let clientInstallScript = 'curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash\n'
|
||||
// createExecShell([host,], clientInstallScript, 300)
|
||||
// })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -175,7 +175,83 @@
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<PlusSupportTip>
|
||||
<el-form-item
|
||||
key="proxyType"
|
||||
label="代理类型"
|
||||
prop="proxyType"
|
||||
>
|
||||
<el-radio-group v-model="hostForm.proxyType" :disabled="!isPlusActive">
|
||||
<el-radio value="">不使用代理</el-radio>
|
||||
<el-radio value="proxyServer">代理服务</el-radio>
|
||||
<el-radio value="jumpHosts">跳板机</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</PlusSupportTip>
|
||||
<el-form-item
|
||||
v-if="hostForm.proxyType === 'jumpHosts'"
|
||||
key="jumpHosts"
|
||||
prop="jumpHosts"
|
||||
label="跳板机"
|
||||
>
|
||||
<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>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="hostForm.proxyType === 'proxyServer'"
|
||||
key="proxyServer"
|
||||
prop="proxyServer"
|
||||
label="代理服务"
|
||||
>
|
||||
<el-select
|
||||
v-model="hostForm.proxyServer"
|
||||
placeholder=""
|
||||
:disabled="!isPlusActive"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="empty_text">
|
||||
<span>无可用代理服务,</span>
|
||||
<el-button type="primary" link @click="toProxy">
|
||||
去添加
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-option
|
||||
v-for="item in proxyList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
>
|
||||
<div class="select_warp">
|
||||
<span>{{ item.name }}</span>
|
||||
<span class="auth_type_text">
|
||||
{{ item.type === 'socks5' ? 'SOCKS5' : 'HTTP' }}
|
||||
</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item
|
||||
key="jumpHosts"
|
||||
prop="jumpHosts"
|
||||
label="跳板机"
|
||||
@@ -204,7 +280,7 @@
|
||||
</el-option>
|
||||
</el-select>
|
||||
</PlusSupportTip>
|
||||
</el-form-item>
|
||||
</el-form-item> -->
|
||||
<el-form-item key="command" prop="command" label="登录指令">
|
||||
<el-input
|
||||
v-model="hostForm.command"
|
||||
@@ -327,16 +403,18 @@ const formField = {
|
||||
consoleUrl: '',
|
||||
remark: '',
|
||||
command: '',
|
||||
jumpHosts: []
|
||||
proxyType: '', // , jumpHosts, proxyServer
|
||||
jumpHosts: [],
|
||||
proxyServer: ''
|
||||
}
|
||||
|
||||
let hostForm = ref({ ...formField })
|
||||
let privateKeyRef = ref(null)
|
||||
let formRef = ref(null)
|
||||
const hostForm = ref({ ...formField })
|
||||
const privateKeyRef = ref(null)
|
||||
const formRef = ref(null)
|
||||
|
||||
let isBatchModify = computed(() => props.isBatchModify)
|
||||
let batchHosts = computed(() => props.batchHosts)
|
||||
let defaultData = computed(() => props.defaultData)
|
||||
const isBatchModify = computed(() => props.isBatchModify)
|
||||
const batchHosts = computed(() => props.batchHosts)
|
||||
const defaultData = computed(() => props.defaultData)
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
group: { required: !isBatchModify.value, message: '选择一个分组' },
|
||||
@@ -364,12 +442,11 @@ const title = computed(() => {
|
||||
return isBatchModify.value ? '批量修改实例' : (defaultData.value ? '修改实例' : '添加实例')
|
||||
})
|
||||
|
||||
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 groupList = computed(() => $store.groupList)
|
||||
const sshList = computed(() => $store.sshList)
|
||||
const hostList = computed(() => $store.hostList)
|
||||
const confHostList = computed(() => hostList.value?.filter(item => item.isConfig))
|
||||
const proxyList = computed(() => $store.proxyList)
|
||||
|
||||
const setDefaultData = () => {
|
||||
if (!defaultData.value) {
|
||||
@@ -388,7 +465,7 @@ const setDefaultData = () => {
|
||||
|
||||
const setBatchDefaultData = () => {
|
||||
if (!isBatchModify.value) return
|
||||
Object.assign(hostForm.value, { ...formField }, { group: '', port: '', username: '', authType: '', jumpHosts: [] })
|
||||
Object.assign(hostForm.value, { ...formField }, { group: '', port: '', username: '', authType: '', proxyType: '', jumpHosts: [], proxyServer: '' })
|
||||
}
|
||||
const handleOpen = async () => {
|
||||
if (isBatchModify.value) {
|
||||
@@ -438,6 +515,11 @@ const userSearch = (keyword, cb) => {
|
||||
cb(res)
|
||||
}
|
||||
|
||||
const toProxy = () => {
|
||||
visible.value = false
|
||||
$router.push({ path: '/setting', query: { tabKey: 'proxy' } })
|
||||
}
|
||||
|
||||
const toCredentials = () => {
|
||||
visible.value = false
|
||||
$router.push({ path: '/credentials' })
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
<el-dropdown-item @click="handleBatchSSH">连接终端</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleBatchModify">批量修改</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleBatchRemove">批量删除</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleBatchOnekey">安装客户端</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -148,13 +147,6 @@ let handleUpdateHost = (defaultData) => {
|
||||
updateHostData.value = defaultData
|
||||
}
|
||||
|
||||
let handleBatchOnekey = async () => {
|
||||
collectSelectHost()
|
||||
if (!selectHosts.value.length) return $message.warning('请选择要批量操作的实例')
|
||||
let ids = selectHosts.value.map(item => item.id).join(',')
|
||||
$router.push({ path: '/onekey', query: { hostIds: ids, execClientInstallScript: 'true' } })
|
||||
}
|
||||
|
||||
let handleBatchExport = () => {
|
||||
collectSelectHost()
|
||||
if (!selectHosts.value.length) return $message.warning('请选择要批量操作的实例')
|
||||
|
||||
343
web/src/views/setting/components/proxy.vue
Normal file
343
web/src/views/setting/components/proxy.vue
Normal file
@@ -0,0 +1,343 @@
|
||||
<template>
|
||||
<div class="proxy-container">
|
||||
<div class="operation-bar">
|
||||
<PlusSupportTip>
|
||||
<el-button
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
:disabled="!isPlusActive"
|
||||
@click="handleAdd"
|
||||
>
|
||||
添加代理
|
||||
</el-button>
|
||||
</PlusSupportTip>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="proxyList"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
empty-text="暂无代理数据"
|
||||
>
|
||||
<el-table-column prop="type" label="类型">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.type === 'socks5' ? 'success' : 'primary'">
|
||||
{{ row.type === 'socks5' ? 'SOCKS5' : 'HTTP' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="名称" />
|
||||
<el-table-column prop="host" label="主机" />
|
||||
<el-table-column prop="port" label="端口" />
|
||||
<el-table-column prop="username" label="用户名">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.username || '--' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="password" label="密码">
|
||||
<template #default="{ row }">
|
||||
<span @click="handleShowPassword(row)">{{ row.displayPassword || '--' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间">
|
||||
<template #default="{ row }">
|
||||
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="250" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="success" @click="handleClone(row)">
|
||||
克隆
|
||||
</el-button>
|
||||
<el-button type="danger" @click="handleDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-width="80px"
|
||||
label-suffix=":"
|
||||
>
|
||||
<el-form-item label="类型" prop="type">
|
||||
<el-select
|
||||
v-model="formData.type"
|
||||
placeholder="请选择代理类型"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<el-option label="HTTP" value="http" />
|
||||
<el-option label="SOCKS5" value="socks5" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input
|
||||
v-model.trim="formData.name"
|
||||
placeholder="请输入代理名称"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="主机" prop="host">
|
||||
<el-input
|
||||
v-model.trim="formData.host"
|
||||
placeholder="请输入主机地址"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="端口" prop="port">
|
||||
<el-input
|
||||
v-model.number="formData.port"
|
||||
placeholder="请输入端口号"
|
||||
type="number"
|
||||
:min="1"
|
||||
:max="65535"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input
|
||||
v-model.trim="formData.username"
|
||||
placeholder="可选,请输入用户名"
|
||||
maxlength="100"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input
|
||||
v-model.trim="formData.password"
|
||||
placeholder="可选,请输入密码"
|
||||
maxlength="200"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, getCurrentInstance, computed } from 'vue'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import PlusSupportTip from '@/components/common/PlusSupportTip.vue'
|
||||
|
||||
const { proxy: { $api, $message, $messageBox, $store } } = getCurrentInstance()
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('添加代理')
|
||||
const formRef = ref(null)
|
||||
const currentEditId = ref(null)
|
||||
const proxyList = computed(() => {
|
||||
if (Array.isArray($store.proxyList)) {
|
||||
return $store.proxyList.map(item => {
|
||||
item.displayPassword = formatPassword(item.password)
|
||||
return item
|
||||
})
|
||||
}
|
||||
return []
|
||||
})
|
||||
const isPlusActive = computed(() => $store.isPlusActive)
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
type: 'socks5',
|
||||
name: '',
|
||||
host: '',
|
||||
port: '',
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
type: [
|
||||
{ required: true, message: '请选择代理类型', trigger: 'change' },
|
||||
],
|
||||
name: [
|
||||
{ required: true, message: '请输入代理名称', trigger: 'blur' },
|
||||
{ min: 1, max: 50, message: '名称长度应在 1 到 50 个字符之间', trigger: 'blur' },
|
||||
],
|
||||
host: [
|
||||
{ required: true, message: '请输入主机地址', trigger: 'blur' },
|
||||
],
|
||||
port: [
|
||||
{ required: true, message: '请输入端口号', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, max: 65535, message: '端口号必须是1-65535之间的整数', trigger: 'blur' },
|
||||
]
|
||||
})
|
||||
|
||||
// 格式化密码显示
|
||||
const formatPassword = (password) => {
|
||||
if (!password) return '-'
|
||||
if (password.length <= 6) {
|
||||
return '*'.repeat(password.length)
|
||||
}
|
||||
const start = password.slice(0, 3)
|
||||
const end = password.slice(-3)
|
||||
const middle = '*'.repeat(password.length - 6)
|
||||
return start + middle + end
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
type: 'socks5',
|
||||
name: '',
|
||||
host: '',
|
||||
port: '',
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
currentEditId.value = null
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
resetForm()
|
||||
dialogTitle.value = '添加代理'
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row) => {
|
||||
resetForm()
|
||||
Object.assign(formData, {
|
||||
type: row.type,
|
||||
name: row.name,
|
||||
host: row.host,
|
||||
port: row.port,
|
||||
username: row.username || '',
|
||||
password: row.password || ''
|
||||
})
|
||||
currentEditId.value = row.id
|
||||
dialogTitle.value = '编辑代理'
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await $messageBox.confirm(
|
||||
`确定要删除代理"${ row.name }"吗?`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await $api.removeProxy(row.id)
|
||||
$message.success('删除成功')
|
||||
await $store.getProxyList()
|
||||
await $store.getHostList() // 后台会移除所有使用该代理的实例的proxyServer字段
|
||||
} catch (error) {
|
||||
// 如果是用户取消操作,不显示错误
|
||||
if (error === 'cancel') {
|
||||
return
|
||||
}
|
||||
console.error('删除代理失败:', error)
|
||||
$message.error('删除代理失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleClone = async (row) => {
|
||||
try {
|
||||
// 构造克隆数据
|
||||
const cloneData = {
|
||||
type: row.type,
|
||||
name: `${ row.name }_克隆`,
|
||||
host: row.host,
|
||||
port: row.port,
|
||||
username: row.username || '',
|
||||
password: row.password || ''
|
||||
}
|
||||
|
||||
// 直接调用新增接口
|
||||
await $api.addProxy(cloneData)
|
||||
$message.success('克隆成功')
|
||||
|
||||
// 刷新代理列表
|
||||
await $store.getProxyList()
|
||||
} catch (error) {
|
||||
console.error('克隆代理失败:', error)
|
||||
$message.error('克隆代理失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
dialogVisible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const valid = await formRef.value.validate()
|
||||
if (!valid) return
|
||||
|
||||
submitLoading.value = true
|
||||
|
||||
if (currentEditId.value) {
|
||||
// 编辑
|
||||
await $api.updateProxy(currentEditId.value, formData)
|
||||
$message.success('修改成功')
|
||||
} else {
|
||||
// 新增
|
||||
await $api.addProxy(formData)
|
||||
$message.success('添加成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
resetForm()
|
||||
await $store.getProxyList()
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
$message.error('操作失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleShowPassword = (row) => {
|
||||
row.displayPassword = row.password
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.proxy-container {
|
||||
.operation-bar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -13,6 +13,9 @@
|
||||
<el-tab-pane label="通知配置" name="notify-config">
|
||||
<NotifyConfig />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="代理服务" name="proxy">
|
||||
<Proxy />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Plus激活" name="plus">
|
||||
<UserPlus />
|
||||
</el-tab-pane>
|
||||
@@ -28,6 +31,7 @@ import Record from './components/record.vue'
|
||||
import User from './components/user.vue'
|
||||
import NotifyConfig from './components/notify-config.vue'
|
||||
import UserPlus from './components/user-plus.vue'
|
||||
import Proxy from './components/proxy.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -153,7 +153,7 @@ const getCommand = async () => {
|
||||
const connectIO = () => {
|
||||
curStatus.value = CONNECTING
|
||||
socket.value = io($serviceURI, {
|
||||
path: props.isSingleWindow ? '/terminal-single-window' : '/terminal',
|
||||
path: '/terminal',
|
||||
forceNew: false,
|
||||
reconnection: false,
|
||||
reconnectionAttempts: 0
|
||||
@@ -165,6 +165,7 @@ const connectIO = () => {
|
||||
socket.value.emit('ws_terminal', { hostId: hostId.value, token: token.value })
|
||||
socket.value.on('terminal_connect_success', () => {
|
||||
socket.value.on('output', (str) => {
|
||||
if (props.isSingleWindow && !isPlusActive.value) return
|
||||
term.value.write(str)
|
||||
terminalText.value += str
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user