mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-10-06 12:33:59 +08:00
feat: 优化server端代码&mobile端文案
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 }`)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
Reference in New Issue
Block a user