feat: 优化server端代码&mobile端文案

This commit is contained in:
chaos-zhu
2026-05-25 21:52:32 +08:00
parent 84ea6951d0
commit d1f4561dab
9 changed files with 87 additions and 356 deletions
+23 -47
View File
@@ -1,16 +1,22 @@
const crypto = require('crypto')
const { RSADecryptAsync } = require('../utils/encrypt')
const { encryptJsonForMobile } = require('../utils/mobile-crypto')
const { HostListDB, FavoriteSftpDB } = require('../utils/db-class')
// `getConnectionOptions` and `getProxyConfig` are lazily required inside
// functions. Loading `../socket/terminal` at module scope pulls in
// `terminal-session`, which expects `global.logger` to exist after app boot.
const hostListDB = new HostListDB().getInstance()
const favoriteSftpDB = new FavoriteSftpDB().getInstance()
function encryptJsonForMobile(payload, key) {
if (!Buffer.isBuffer(key) || key.length !== 32) {
throw new Error('temporary key must be 32 bytes')
}
const iv = crypto.randomBytes(12)
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv)
const plaintext = Buffer.from(JSON.stringify(payload), 'utf8')
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()])
const tag = cipher.getAuthTag()
function normalizePort(port) {
const numericPort = Number(port)
return Number.isFinite(numericPort) && numericPort > 0 ? numericPort : 22
return {
alg: 'AES-256-GCM',
iv: iv.toString('base64'),
tag: tag.toString('base64'),
ciphertext: ciphertext.toString('base64')
}
}
function normalizeMobileAuthPayload(hostId, name, authInfo = {}) {
@@ -23,7 +29,7 @@ function normalizeMobileAuthPayload(hostId, name, authInfo = {}) {
hostId,
name,
host: host || '',
port: normalizePort(port),
port: Number(port),
username: username || '',
authType,
password: authType === 'password' ? authInfo.password || '' : '',
@@ -42,19 +48,14 @@ function normalizeMobileProxy(proxy = {}) {
name: proxy.name || '',
type: proxy.type,
host: proxy.host || '',
port: normalizePort(proxy.port),
port: Number(proxy.port),
username: proxy.username || '',
password: proxy.password || ''
}
}
function normalizeMobileJumpHost(jumpHost) {
const authInfo = jumpHost.authInfo || jumpHost
return normalizeMobileAuthPayload(
jumpHost.hostId || jumpHost._id || authInfo.hostId || authInfo._id,
jumpHost.name || authInfo.name,
authInfo
)
function normalizeMobileJumpHost({ hostId, name, ...authInfo }) {
return normalizeMobileAuthPayload(hostId, name, authInfo)
}
function toMobileSshPayload(hostId, name, authInfo, topology = {}) {
@@ -139,11 +140,7 @@ async function getMobileSshConnection({ request, res }) {
const tempKeyText = await RSADecryptAsync(encryptedKey)
const tempKey = Buffer.from(tempKeyText, 'base64')
const { getConnectionOptions } = require('../socket/terminal')
const { authInfo, name } = await getConnectionOptions(hostId)
const hostInfo = await hostListDB.findOneAsync({ _id: hostId })
if (!hostInfo) {
throw new Error(`Host with ID ${ hostId } not found`)
}
const { authInfo, name, hostInfo } = await getConnectionOptions(hostId)
const topology = await getMobileConnectionTopology(hostInfo)
const payload = toMobileSshPayload(hostId, name, authInfo, topology)
const data = encryptJsonForMobile(payload, tempKey)
@@ -155,29 +152,8 @@ async function getMobileSshConnection({ request, res }) {
}
}
async function getMobileSftpFavorites({ params, request, res }) {
try {
const hostId = params?.hostId || request.query?.hostId
if (!hostId) {
return res.fail({ msg: 'missing hostId' })
}
const favorites = await favoriteSftpDB.findAsync(
{ hostId },
{ sort: { createTime: -1 } }
)
return res.success({ data: favorites, msg: 'success' })
} catch (error) {
logger.error('getMobileSftpFavorites error:', error.message)
return res.fail({ msg: error.message || 'mobile sftp favorites failed' })
}
}
module.exports = {
getMobileSshConnection,
getMobileSftpFavorites,
getMobileConnectionTopology,
normalizePort,
normalizeMobileAuthPayload,
normalizeMobileProxy,
toMobileSshPayload
getMobileSshConnection
}
+24
View File
@@ -0,0 +1,24 @@
const { HostListDB, CredentialsDB, FavoriteSftpDB } = require('../utils/db-class')
const favoriteSftpDB = new FavoriteSftpDB().getInstance()
async function getSftpFavorites({ params, request, res }) {
try {
const hostId = params?.hostId || request.query?.hostId
if (!hostId) {
return res.fail({ msg: 'missing hostId' })
}
const favorites = await favoriteSftpDB.findAsync(
{ hostId },
{ sort: { createTime: -1 } }
)
return res.success({ data: favorites, msg: 'success' })
} catch (error) {
logger.error('getSftpFavorites error:', error.message)
return res.fail({ msg: error.message || 'mobile sftp favorites failed' })
}
}
module.exports = {
getSftpFavorites
}
+37 -32
View File
@@ -1,4 +1,5 @@
const { getSSHList, addSSH, updateSSH, removeSSH, getCommand, decryptPrivateKey, getRdpToken } = require('../controller/ssh')
const { getSftpFavorites } = require('../controller/sftp')
const { getHostList, addHost, updateHost, batchUpdateHost, removeHost, importHost, updateLastConnectTime } = require('../controller/host')
const { login, getpublicKey, updatePwd, getEasynodeVersion, getMFA2Status, getMFA2Code, enableMFA2, disableMFA2, getPlusInfo, getPlusDiscount, getPlusConf, updatePlusKey } = require('../controller/user')
const { getNotifyConfig, updateNotifyConfig, getNotifyList, updateNotifyList } = require('../controller/notify')
@@ -9,10 +10,10 @@ const { getOnekeyRecord, removeOnekeyRecord } = require('../controller/onekey')
const { getLog, saveIpWhiteList, removeSomeLoginRecords, revokeLoginSid } = require('../controller/sessionLog')
const { getAIConfig, saveAIConfig, getAIModels, getChatHistory, saveChatHistory, removeChatHistory } = require('../controller/chat')
const { getProxyList, addProxy, updateProxy, removeProxy } = require('../controller/proxy')
const { getTerminalConfig, saveTerminalConfig } = require('../controller/terminal-config')
const { getServerListConfig, saveServerListConfig } = require('../controller/server-list-config')
const { getSuspendedSessions, getTerminalSessionConfig, updateTerminalSessionConfig } = require('../controller/terminal')
const { getMobileSshConnection, getMobileSftpFavorites } = require('../controller/mobile')
const { getTerminalConfig, saveTerminalConfig } = require('../controller/terminal-config')
const { getServerListConfig, saveServerListConfig } = require('../controller/server-list-config')
const { getSuspendedSessions, getTerminalSessionConfig, updateTerminalSessionConfig } = require('../controller/terminal')
const { getMobileSshConnection } = require('../controller/mobile')
const ssh = [
{
@@ -375,9 +376,9 @@ const serverListConfig = [
}
]
const terminal = [
{
method: 'get',
const terminal = [
{
method: 'get',
path: '/suspended-sessions',
controller: getSuspendedSessions
},
@@ -390,25 +391,28 @@ const terminal = [
method: 'post',
path: '/terminal-session-config',
controller: updateTerminalSessionConfig
}
]
const mobile = [
{
method: 'post',
path: '/mobile/ssh-connection',
controller: getMobileSshConnection
},
{
method: 'get',
path: '/mobile/sftp-favorites/:hostId',
controller: getMobileSftpFavorites
}
]
module.exports = [].concat(
ssh,
host,
}
]
const mobile = [
{
method: 'post',
path: '/mobile/ssh-connection',
controller: getMobileSshConnection
}
]
const sftp = [
{
method: 'get',
path: '/sftp/favorites/:hostId',
controller: getSftpFavorites
}
]
module.exports = [].concat(
ssh,
host,
user,
notify,
group,
@@ -417,9 +421,10 @@ module.exports = [].concat(
onekey,
log,
aiConfig,
proxy,
terminalConfig,
serverListConfig,
terminal,
mobile
)
proxy,
terminalConfig,
serverListConfig,
terminal,
mobile,
sftp
)
+1 -1
View File
@@ -31,7 +31,7 @@ async function getConnectionOptions(hostId) {
authInfo.authType = authType
authInfo[authType] = await AESDecryptAsync(hostInfo[authType])
}
return { authInfo, name }
return { authInfo, name, hostInfo }
} catch (err) {
throw new Error(`解密认证信息失败: ${ err.message }`)
}
-44
View File
@@ -1,44 +0,0 @@
const crypto = require('crypto')
function assertTempKey(key) {
if (!Buffer.isBuffer(key) || key.length !== 32) {
throw new Error('temporary key must be 32 bytes')
}
}
function encryptJsonForMobile(payload, key) {
assertTempKey(key)
const iv = crypto.randomBytes(12)
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv)
const plaintext = Buffer.from(JSON.stringify(payload), 'utf8')
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()])
const tag = cipher.getAuthTag()
return {
alg: 'AES-256-GCM',
iv: iv.toString('base64'),
tag: tag.toString('base64'),
ciphertext: ciphertext.toString('base64')
}
}
function decryptMobileJsonForTest(envelope, key) {
assertTempKey(key)
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
key,
Buffer.from(envelope.iv, 'base64')
)
decipher.setAuthTag(Buffer.from(envelope.tag, 'base64'))
const plaintext = Buffer.concat([
decipher.update(Buffer.from(envelope.ciphertext, 'base64')),
decipher.final()
])
return JSON.parse(plaintext.toString('utf8'))
}
module.exports = {
assertTempKey,
encryptJsonForMobile,
decryptMobileJsonForTest
}
-25
View File
@@ -1,25 +0,0 @@
const assert = require('assert')
const { encryptJsonForMobile, decryptMobileJsonForTest, assertTempKey } = require('../app/utils/mobile-crypto')
function testRejectsShortKey() {
assert.throws(() => assertTempKey(Buffer.alloc(16)), /temporary key must be 32 bytes/)
}
function testEncryptsAndDecryptsJson() {
const key = Buffer.from('0123456789abcdef0123456789abcdef')
const payload = { host: '127.0.0.1', password: 'secret' }
const envelope = encryptJsonForMobile(payload, key)
assert.strictEqual(envelope.alg, 'AES-256-GCM')
assert.ok(envelope.iv)
assert.ok(envelope.tag)
assert.ok(envelope.ciphertext)
assert.ok(!JSON.stringify(envelope).includes('secret'))
const decoded = decryptMobileJsonForTest(envelope, key)
assert.deepStrictEqual(decoded, payload)
}
testRejectsShortKey()
testEncryptsAndDecryptsJson()
console.log('test-mobile-crypto passed')
-205
View File
@@ -1,205 +0,0 @@
const assert = require('assert')
const { toMobileSshPayload } = require('../app/controller/mobile')
function testPasswordPayload() {
const payload = toMobileSshPayload('h1', 'prod', {
host: '10.0.0.2',
port: 22,
username: 'root',
authType: 'password',
password: 'p@ss'
})
assert.deepStrictEqual(payload, {
hostId: 'h1',
name: 'prod',
host: '10.0.0.2',
port: 22,
username: 'root',
authType: 'password',
password: 'p@ss',
privateKey: '',
passphrase: '',
proxyType: '',
proxy: null,
jumpHosts: []
})
}
function testPrivateKeyPayload() {
const payload = toMobileSshPayload('h2', 'keyhost', {
host: '10.0.0.3',
port: 2222,
username: 'ubuntu',
authType: 'privateKey',
privateKey: 'KEY',
passphrase: 'phrase'
})
assert.strictEqual(payload.authType, 'privateKey')
assert.strictEqual(payload.privateKey, 'KEY')
assert.strictEqual(payload.password, '')
assert.strictEqual(payload.passphrase, 'phrase')
assert.strictEqual(payload.proxyType, '')
assert.strictEqual(payload.proxy, null)
assert.deepStrictEqual(payload.jumpHosts, [])
}
function testRejectsUnsupportedAuth() {
assert.throws(() => toMobileSshPayload('h3', 'unsupported', {
host: '10.0.0.4',
port: 22,
username: 'root',
authType: 'keyboard'
}), /unsupported mobile ssh auth type/)
}
function testSocks5ProxyPayload() {
const payload = toMobileSshPayload('h4', 'proxied', {
host: '10.0.0.5',
port: '2200',
username: 'deploy',
authType: 'password',
password: 'secret'
}, {
proxyType: 'proxyServer',
proxy: {
id: 'p1',
name: 'edge-proxy',
type: 'socks5',
host: '127.0.0.1',
port: '1080',
username: 'proxy-user',
password: 'proxy-pass'
}
})
assert.deepStrictEqual(payload.proxy, {
id: 'p1',
name: 'edge-proxy',
type: 'socks5',
host: '127.0.0.1',
port: 1080,
username: 'proxy-user',
password: 'proxy-pass'
})
assert.strictEqual(payload.proxyType, 'proxyServer')
assert.deepStrictEqual(payload.jumpHosts, [])
}
function testHttpProxyPayload() {
const payload = toMobileSshPayload('h4-http', 'http-proxied', {
host: '10.0.0.50',
port: 22,
username: 'deploy',
authType: 'password',
password: 'secret'
}, {
proxyType: 'proxyServer',
proxy: {
id: 'p-http',
name: 'http-proxy',
type: 'http',
host: '127.0.0.1',
port: '8080',
username: 'proxy-user',
password: 'proxy-pass'
}
})
assert.deepStrictEqual(payload.proxy, {
id: 'p-http',
name: 'http-proxy',
type: 'http',
host: '127.0.0.1',
port: 8080,
username: 'proxy-user',
password: 'proxy-pass'
})
assert.strictEqual(payload.proxyType, 'proxyServer')
assert.deepStrictEqual(payload.jumpHosts, [])
}
function testJumpHostsPayload() {
const payload = toMobileSshPayload('h5', 'target', {
host: '10.0.0.6',
port: 22,
username: 'app',
authType: 'privateKey',
privateKey: 'TARGET_KEY',
password: 'ignored',
passphrase: ''
}, {
proxyType: 'jumpHosts',
jumpHosts: [
{
hostId: 'j1',
name: 'bastion',
host: '10.0.0.7',
port: '2222',
username: 'jump',
authType: 'password',
password: 'jump-pass',
privateKey: 'ignored'
}
]
})
assert.strictEqual(payload.proxyType, 'jumpHosts')
assert.strictEqual(payload.proxy, null)
assert.deepStrictEqual(payload.jumpHosts, [
{
hostId: 'j1',
name: 'bastion',
host: '10.0.0.7',
port: 2222,
username: 'jump',
authType: 'password',
password: 'jump-pass',
privateKey: '',
passphrase: ''
}
])
}
function testRejectsUnsupportedProxyType() {
assert.throws(() => toMobileSshPayload('h6', 'bad-proxy', {
host: '10.0.0.8',
port: 22,
username: 'root',
authType: 'password',
password: 'secret'
}, {
proxyType: 'proxyServer',
proxy: {
id: 'p2',
name: 'https-proxy',
type: 'https',
host: '127.0.0.1',
port: 8080
}
}), /unsupported mobile proxy type: https/)
}
function testRejectsEmptyJumpHostChain() {
assert.throws(() => toMobileSshPayload('h7', 'empty-jumps', {
host: '10.0.0.9',
port: 22,
username: 'root',
authType: 'password',
password: 'secret'
}, {
proxyType: 'jumpHosts',
jumpHosts: []
}), /mobile jump host chain is empty/)
}
testPasswordPayload()
testPrivateKeyPayload()
testRejectsUnsupportedAuth()
testSocks5ProxyPayload()
testHttpProxyPayload()
testJumpHostsPayload()
testRejectsUnsupportedProxyType()
testRejectsEmptyJumpHostChain()
console.log('test-mobile-ssh-payload passed')