Files
easynode/server/app/server.js
2026-02-08 16:42:45 +08:00

183 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const Koa = require('koa')
const compose = require('koa-compose') // 组合中间件,简化写法
const http = require('http')
const https = require('https')
const fs = require('fs')
const { httpPort, httpsPort, enableHttps, sslCertPath, sslKeyPath } = require('./config')
const middlewares = require('./middlewares')
const { startRdpServer } = require('./rdp-server')
const wsTerminal = require('./socket/terminal')
const wsSftpV2 = require('./socket/sftp-v2')
const wsDocker = require('./socket/docker')
const wsOnekey = require('./socket/onekey')
const wsServerStatus = require('./socket/server-status')
const wsFileTransfer = require('./socket/file-transfer')
const { throwError, isAllowedIp } = require('./utils/tools')
const { SessionDB } = require('./utils/db-class')
const { parseCookies } = require('./utils/verify-auth')
const { generateSelfSignedCert } = require('./utils/ssl-cert')
const createRdpProxyMiddleware = require('./middlewares/rdp-proxy')
const sessionDB = new SessionDB().getInstance()
const createServer = () => {
const app = new Koa()
// 创建HTTP服务器
const httpServer = http.createServer(app.callback())
httpServer.setMaxListeners(20)
// 创建HTTPS服务器(如果启用)
let httpsServer = null
if (enableHttps === 1) {
// 模式1: 自签证书
try {
const { cert, key } = generateSelfSignedCert()
const httpsOptions = { cert, key }
httpsServer = https.createServer(httpsOptions, app.callback())
logger.info('🔒 HTTPS服务器已配置(自签名证书)')
} catch (error) {
logger.error('自签名证书生成失败:', error.message)
process.exit(1)
}
} else if (enableHttps === 2) {
// 模式2: 传入证书路径
try {
// 验证证书文件路径是否配置
if (!sslCertPath || !sslKeyPath) {
logger.error('ENABLE_HTTPS=2 时,必须配置 SSL_CERT_PATH 和 SSL_KEY_PATH 环境变量')
process.exit(1)
}
// 验证证书文件是否存在
if (!fs.existsSync(sslCertPath)) {
logger.error(`SSL证书文件不存在: ${ sslCertPath }`)
process.exit(1)
}
if (!fs.existsSync(sslKeyPath)) {
logger.error(`SSL私钥文件不存在: ${ sslKeyPath }`)
process.exit(1)
}
// 读取SSL证书
const httpsOptions = {
cert: fs.readFileSync(sslCertPath),
key: fs.readFileSync(sslKeyPath)
}
httpsServer = https.createServer(httpsOptions, app.callback())
logger.info('🔒 HTTPS服务器已配置(自定义证书)')
} catch (error) {
logger.error('HTTPS服务器配置失败:', error.message)
process.exit(1)
}
} else {
// 模式0: 关闭HTTPS
logger.info('HTTPS已关闭')
}
// 添加RDP WebSocket代理
const rdpProxy = createRdpProxyMiddleware()
// WebSocket升级处理函数
const handleRdpWsUpgrade = async (request, socket, head) => {
// 只处理WebSocket升级请求的代理RDP只需要WebSocket
// 安全说明:
// 1. RDP token 是通过 /get-rdp-token API 获取的,该 API 受 auth 中间件保护,只有登录用户才能获取
// 2. RDP token 由 guacamole-lite 使用 AES-256-CBC 加密包含连接信息guacamole-lite 会验证 token 有效性
// 3. 这里只需要验证 IP 白名单,防止 token 泄露后被非授权 IP 使用【0127增强: 验证session】
if (request.url.startsWith('/rdp-proxy')) {
try {
// 验证 IP 白名单
const requestIP = request.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
request.socket.remoteAddress
if (!isAllowedIp(requestIP)) {
logger.warn(`RDP 连接被拒绝: IP ${ requestIP } 不在白名单中`)
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n')
socket.destroy()
return
}
// 验证 session
const cookies = request.headers.cookie
const { session } = parseCookies(cookies)
const sessionRecord = await sessionDB.findOneAsync({ session })
// 是否无效/注销/过期的token
if (
!session ||
!sessionRecord ||
sessionRecord.revoked !== false ||
sessionRecord.expireAt < Date.now()
) {
logger.warn(`RDP 连接被拒绝: IP ${ requestIP } 不在白名单中`)
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n')
socket.destroy()
return
}
// 验证通过,转发请求到 guacamole-lite
// guacamole-lite 会验证 URL 中的加密 token
console.log('RDP 代理转发请求初步验证成功,开始转发...')
rdpProxy.upgrade(request, socket, head)
} catch (error) {
logger.error('RDP 代理异常:', error.message)
socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n')
socket.destroy()
}
}
// 对于非 /rdp-proxy 路径, Socket.IO 的内部 upgrade 监听器自动处理
}
httpServer.on('upgrade', handleRdpWsUpgrade)
if (httpsServer) {
httpsServer.on('upgrade', handleRdpWsUpgrade)
}
serverHandler(app, httpServer, httpsServer)
// ws一直报跨域的错误参照官方文档使用createServer API创建服务
httpServer.listen(httpPort, () => {
logger.info(`Server(http) is running on: http://localhost:${ httpPort }`)
})
// 启动HTTPS服务器
if (httpsServer) {
httpsServer.listen(httpsPort, () => {
logger.info(`Server(https) is running on: https://localhost:${ httpsPort }`)
})
}
// 启动独立的RDP服务
startRdpServer()
}
function registServer(targetServer) {
// 为HTTP服务器添加WebSocket支持
wsTerminal(targetServer) // 终端
wsSftpV2(targetServer) // sftp-v2
wsDocker(targetServer) // docker
wsOnekey(targetServer) // 一键指令
wsServerStatus(targetServer) // 服务器状态监控
wsFileTransfer(targetServer) // 文件传输
}
// 服务
function serverHandler(app, server, httpsServer) {
app.proxy = true // 用于nginx反代时获取真实客户端ip
registServer(server)
if (httpsServer) registServer(httpsServer)
app.context.throwError = throwError // 常用方法挂载全局ctx上
app.use(compose(middlewares))
// 捕获error.js模块抛出的服务错误
app.on('error', (err, ctx) => {
ctx.status = 500
ctx.body = {
status: ctx.status,
message: `Program Error${ err.message }`
}
})
}
module.exports = {
createServer
}