diff --git a/server/.env.template b/server/.env.template index 03c8c62..2aa3f6b 100644 --- a/server/.env.template +++ b/server/.env.template @@ -1,2 +1,5 @@ # 启动debug日志 0:关闭 1:开启 DEBUG=1 + +# 访问IP限制 +allowedIPs=['127.0.0.1'] \ No newline at end of file diff --git a/server/app/middlewares/auth.js b/server/app/middlewares/auth.js index b12f0c8..6308c02 100644 --- a/server/app/middlewares/auth.js +++ b/server/app/middlewares/auth.js @@ -10,11 +10,11 @@ consola.info('路由白名单:', whitePath) const useAuth = async ({ request, res }, next) => { const { path, headers: { token } } = request consola.info('verify path: ', path) - if(whitePath.includes(path)) return next() - if(!token) return res.fail({ msg: '未登录', status: 403 }) + if (whitePath.includes(path)) return next() + if (!token) return res.fail({ msg: '未登录', status: 403 }) // 验证token const { code, msg } = await verifyAuthSync(token, request.ip) - switch(code) { + switch (code) { case 1: return await next() case -1: diff --git a/server/app/middlewares/index.js b/server/app/middlewares/index.js index f7932cd..0e3ff7d 100644 --- a/server/app/middlewares/index.js +++ b/server/app/middlewares/index.js @@ -1,3 +1,4 @@ +const ipFilter = require('./ipFilter') // IP过滤 const responseHandler = require('./response') // 统一返回格式, 错误捕获 const useAuth = require('./auth') // 鉴权 // const useCors = require('./cors') // 处理跨域[暂时禁止] @@ -8,8 +9,8 @@ const useStatic = require('./static') // 静态目录 const compress = require('./compress') // br/gzip压缩 const history = require('./history') // vue-router的history模式 -// 注意注册顺序 module.exports = [ + ipFilter, compress, history, useStatic, // staic先注册,不然会被jwt拦截 diff --git a/server/app/middlewares/ipFilter.js b/server/app/middlewares/ipFilter.js new file mode 100644 index 0000000..d0f67ed --- /dev/null +++ b/server/app/middlewares/ipFilter.js @@ -0,0 +1,16 @@ +// 白名单IP +const fs = require('fs') +const path = require('path') +const { isAllowedIp } = require('../utils/tools') + +const htmlPath = path.join(__dirname, '../template/ipForbidden.html') +const ipForbiddenHtml = fs.readFileSync(htmlPath, 'utf8') + +const ipFilter = async (ctx, next) => { + // console.log('requestIP:', ctx.request.ip) + if (isAllowedIp(ctx.request.ip)) return await next() + ctx.status = 403 + ctx.body = ipForbiddenHtml +} + +module.exports = ipFilter diff --git a/server/app/middlewares/static.js b/server/app/middlewares/static.js index 11542fc..590fa89 100644 --- a/server/app/middlewares/static.js +++ b/server/app/middlewares/static.js @@ -5,7 +5,7 @@ const useStatic = koaStatic(staticDir, { maxage: 1000 * 60 * 60 * 24 * 30, gzip: true, setHeaders: (res, path) => { - if(path && path.endsWith('.html')) { + if (path && path.endsWith('.html')) { res.setHeader('Cache-Control', 'max-age=0') } } diff --git a/server/app/server.js b/server/app/server.js index 2789e90..c462e09 100644 --- a/server/app/server.js +++ b/server/app/server.js @@ -5,7 +5,6 @@ const { httpPort } = require('./config') const middlewares = require('./middlewares') const wsTerminal = require('./socket/terminal') const wsSftp = require('./socket/sftp') -// const wsHostStatus = require('./socket/host-status') const wsClientInfo = require('./socket/clients') const wsOnekey = require('./socket/onekey') const { throwError } = require('./utils/tools') @@ -25,7 +24,6 @@ function serverHandler(app, server) { app.proxy = true // 用于nginx反代时获取真实客户端ip wsTerminal(server) // 终端 wsSftp(server) // sftp - // wsHostStatus(server) // 终端侧边栏host信息(单个host) wsOnekey(server) // 一键指令 wsClientInfo(server) // 客户端信息 app.context.throwError = throwError // 常用方法挂载全局ctx上 diff --git a/server/app/socket/clients.js b/server/app/socket/clients.js index 643d226..dbbdecc 100644 --- a/server/app/socket/clients.js +++ b/server/app/socket/clients.js @@ -3,6 +3,7 @@ const { io: ClientIO } = require('socket.io-client') const { readHostList } = require('../utils/storage') const { clientPort } = require('../config') const { verifyAuthSync } = require('../utils/verify-auth') +const { isAllowedIp } = require('../utils/tools') let clientSockets = [] let clientsData = {} @@ -66,9 +67,14 @@ module.exports = (httpServer) => { serverIo.on('connection', (socket) => { // 前者兼容nginx反代, 后者兼容nodejs自身服务 - let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address + let requestIP = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address + if (!isAllowedIp(requestIP)) { + socket.emit('ip_forbidden', 'IP地址不在白名单中') + socket.disconnect() + return + } socket.on('init_clients_data', async ({ token }) => { - const { code, msg } = await verifyAuthSync(token, clientIp) + const { code, msg } = await verifyAuthSync(token, requestIP) if (code !== 1) { socket.emit('token_verify_fail', msg || '鉴权失败') socket.disconnect() diff --git a/server/app/socket/host-status.js b/server/app/socket/host-status.js deleted file mode 100644 index 6122b69..0000000 --- a/server/app/socket/host-status.js +++ /dev/null @@ -1,74 +0,0 @@ -const { Server: ServerIO } = require('socket.io') -const { io: ClientIO } = require('socket.io-client') -const { clientPort } = require('../config') -const { verifyAuthSync } = require('../utils/verify-auth') - -let hostSockets = {} - -function getHostInfo(serverSocket, host) { - let hostSocket = ClientIO(`http://${ host }:${ clientPort }`, { - path: '/client/os-info', - forceNew: false, - timeout: 5000, - reconnectionDelay: 3000, - reconnectionAttempts: 3 - }) - // 将与客户端连接的socket实例保存起来,web端断开时关闭与客户端的连接 - hostSockets[serverSocket.id] = hostSocket - - hostSocket - .on('connect', () => { - consola.success('host-status-socket连接成功:', host) - hostSocket.on('client_data', (data) => { - serverSocket.emit('host_data', data) - }) - hostSocket.on('client_error', () => { - serverSocket.emit('host_data', null) - }) - }) - .on('connect_error', (error) => { - consola.error('host-status-socket连接[失败]:', host, error.message) - serverSocket.emit('host_data', null) - }) - .on('disconnect', () => { - consola.info('host-status-socket连接[断开]:', host) - serverSocket.emit('host_data', null) - }) -} - -module.exports = (httpServer) => { - const serverIo = new ServerIO(httpServer, { - path: '/host-status', - cors: { - origin: '*' // 需配置跨域 - } - }) - - serverIo.on('connection', (serverSocket) => { - // 前者兼容nginx反代, 后者兼容nodejs自身服务 - let clientIp = serverSocket.handshake.headers['x-forwarded-for'] || serverSocket.handshake.address - serverSocket.on('init_host_data', async ({ token, host }) => { - // 校验登录态 - const { code, msg } = await verifyAuthSync(token, clientIp) - if(code !== 1) { - serverSocket.emit('token_verify_fail', msg || '鉴权失败') - serverSocket.disconnect() - return - } - - // 获取客户端数据 - getHostInfo(serverSocket, host) - - consola.info('host-status-socket连接socketId: ', serverSocket.id, 'host-status-socket已连接数: ', Object.keys(hostSockets).length) - - // 关闭连接 - serverSocket.on('disconnect', () => { - // 当web端与服务端断开连接时, 服务端与每个客户端的socket也应该断开连接 - let socket = hostSockets[serverSocket.id] - socket.close && socket.close() - delete hostSockets[serverSocket.id] - consola.info('host-status-socket剩余连接数: ', Object.keys(hostSockets).length) - }) - }) - }) -} diff --git a/server/app/socket/onekey.js b/server/app/socket/onekey.js index 82d8e5b..94b36ee 100644 --- a/server/app/socket/onekey.js +++ b/server/app/socket/onekey.js @@ -5,6 +5,7 @@ const { readSSHRecord, readHostList, writeOneKeyRecord } = require('../utils/sto const { verifyAuthSync } = require('../utils/verify-auth') const { shellThrottle } = require('../utils/tools') const { AESDecryptSync } = require('../utils/encrypt') +const { isAllowedIp } = require('../utils/tools') const execStatusEnum = { connecting: '连接中', @@ -90,7 +91,12 @@ module.exports = (httpServer) => { }) serverIo.on('connection', (socket) => { // 前者兼容nginx反代, 后者兼容nodejs自身服务 - let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address + let requestIP = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address + if (!isAllowedIp(requestIP)) { + socket.emit('ip_forbidden', 'IP地址不在白名单中') + socket.disconnect() + return + } consola.success('onekey-terminal websocket 已连接') if (isExecuting) { socket.emit('create_fail', '正在执行中, 请稍后再试') @@ -99,7 +105,7 @@ module.exports = (httpServer) => { } isExecuting = true socket.on('create', async ({ hosts, token, command, timeout }) => { - const { code } = await verifyAuthSync(token, clientIp) + const { code } = await verifyAuthSync(token, requestIP) if (code !== 1) { socket.emit('token_verify_fail') socket.disconnect() diff --git a/server/app/socket/sftp.js b/server/app/socket/sftp.js index 8c94ac4..1165452 100644 --- a/server/app/socket/sftp.js +++ b/server/app/socket/sftp.js @@ -7,6 +7,7 @@ const { sftpCacheDir } = require('../config') const { verifyAuthSync } = require('../utils/verify-auth') const { AESDecryptSync } = require('../utils/encrypt') const { readSSHRecord, readHostList } = require('../utils/storage') +const { isAllowedIp } = require('../utils/tools') // 读取切片 const pipeStream = (path, writeStream) => { @@ -23,7 +24,7 @@ const pipeStream = (path, writeStream) => { function listenInput(sftpClient, socket) { socket.on('open_dir', async (path, tips = true) => { const exists = await sftpClient.exists(path) - if(!exists) return socket.emit('not_exists_dir', tips ? '目录不存在或当前不可访问' : '') + if (!exists) return socket.emit('not_exists_dir', tips ? '目录不存在或当前不可访问' : '') try { let dirLs = await sftpClient.list(path) socket.emit('dir_ls', dirLs, path) @@ -34,7 +35,7 @@ function listenInput(sftpClient, socket) { }) socket.on('rm_dir', async (path) => { const exists = await sftpClient.exists(path) - if(!exists) return socket.emit('not_exists_dir', '目录不存在或当前不可访问') + if (!exists) return socket.emit('not_exists_dir', '目录不存在或当前不可访问') try { let res = await sftpClient.rmdir(path, true) // 递归删除 socket.emit('rm_success', res) @@ -45,7 +46,7 @@ function listenInput(sftpClient, socket) { }) socket.on('rm_file', async (path) => { const exists = await sftpClient.exists(path) - if(!exists) return socket.emit('not_exists_dir', '文件不存在或当前不可访问') + if (!exists) return socket.emit('not_exists_dir', '文件不存在或当前不可访问') try { let res = await sftpClient.delete(path) socket.emit('rm_success', res) @@ -65,13 +66,13 @@ function listenInput(sftpClient, socket) { socket.on('down_file', async ({ path, name, size, target = 'down' }) => { // target: down or preview const exists = await sftpClient.exists(path) - if(!exists) return socket.emit('not_exists_dir', '文件不存在或当前不可访问') + if (!exists) return socket.emit('not_exists_dir', '文件不存在或当前不可访问') try { const localPath = rawPath.join(sftpCacheDir, name) let timer = null let res = await sftpClient.fastGet(path, localPath, { step: step => { - if(timer) return + if (timer) return timer = setTimeout(() => { const percent = Math.ceil((step / size) * 100) // 下载进度为服务器下载到服务端的进度,前端无需*2 console.log(`从服务器下载进度:${ percent }%`) @@ -83,7 +84,7 @@ function listenInput(sftpClient, socket) { consola.success('sftp下载成功: ', res) let buffer = fs.readFileSync(localPath) let data = { buffer, name } - switch(target) { + switch (target) { case 'down': socket.emit('down_file_success', data) break @@ -102,7 +103,7 @@ function listenInput(sftpClient, socket) { socket.on('up_file', async ({ targetPath, fullPath, name, file }) => { // console.log({ targetPath, fullPath, name, file }) const exists = await sftpClient.exists(targetPath) - if(!exists) return socket.emit('not_exists_dir', '目录不存在或当前不可访问') + if (!exists) return socket.emit('not_exists_dir', '目录不存在或当前不可访问') try { const localPath = rawPath.join(sftpCacheDir, name) fs.writeFileSync(localPath, file) @@ -137,7 +138,7 @@ function listenInput(sftpClient, socket) { socket.on('create_cache_dir', async ({ targetDirPath, name }) => { // console.log({ targetDirPath, name }) const exists = await sftpClient.exists(targetDirPath) - if(!exists) return socket.emit('not_exists_dir', '目录不存在或当前不可访问') + if (!exists) return socket.emit('not_exists_dir', '目录不存在或当前不可访问') md5List = [] const localPath = rawPath.join(sftpCacheDir, name) fs.emptyDirSync(localPath) // 不存在会创建,存在则清空 @@ -178,7 +179,7 @@ function listenInput(sftpClient, socket) { let timer = null let res = await sftpClient.fastPut(resultFilePath, targetFilePath, { step: step => { - if(timer) return + if (timer) return timer = setTimeout(() => { const percent = Math.ceil((step / size) * 100) console.log(`上传服务器进度:${ percent }%`) @@ -210,13 +211,18 @@ module.exports = (httpServer) => { }) serverIo.on('connection', (socket) => { // 前者兼容nginx反代, 后者兼容nodejs自身服务 - let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address + let requestIP = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address + if (!isAllowedIp(requestIP)) { + socket.emit('ip_forbidden', 'IP地址不在白名单中') + socket.disconnect() + return + } let sftpClient = new SFTPClient() consola.success('terminal websocket 已连接') socket.on('create', async ({ host: ip, token }) => { - const { code } = await verifyAuthSync(token, clientIp) - if(code !== 1) { + const { code } = await verifyAuthSync(token, requestIP) + if (code !== 1) { socket.emit('token_verify_fail') socket.disconnect() return diff --git a/server/app/socket/terminal.js b/server/app/socket/terminal.js index 4c6fd91..997716e 100644 --- a/server/app/socket/terminal.js +++ b/server/app/socket/terminal.js @@ -4,6 +4,7 @@ const { verifyAuthSync } = require('../utils/verify-auth') const { AESDecryptSync } = require('../utils/encrypt') const { readSSHRecord, readHostList } = require('../utils/storage') const { asyncSendNotice } = require('../utils/notify') +const { isAllowedIp } = require('../utils/tools') function createInteractiveShell(socket, sshClient) { return new Promise((resolve) => { @@ -113,11 +114,16 @@ module.exports = (httpServer) => { }) serverIo.on('connection', (socket) => { // 前者兼容nginx反代, 后者兼容nodejs自身服务 - let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address + 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 sshClient = null socket.on('create', async ({ host: ip, token }) => { - const { code } = await verifyAuthSync(token, clientIp) + const { code } = await verifyAuthSync(token, requestIP) if (code !== 1) { socket.emit('token_verify_fail') socket.disconnect() diff --git a/server/app/template/ipForbidden.html b/server/app/template/ipForbidden.html new file mode 100644 index 0000000..cddbf5d --- /dev/null +++ b/server/app/template/ipForbidden.html @@ -0,0 +1,43 @@ + + + +
+ + +抱歉,您没有权限访问此页面。
+