Compare commits

4 Commits
Author SHA1 Message Date
lee 5ab9a52628 fix: 新增 NetBusinessServiceImpl 实现类,修复 Spring Bean 注入失败
问题:BusinessTasks依赖注入NetBusinessService,但缺少@Service实现类
导致启动报错 NoSuchBeanDefinitionException

解决:从1.20反编译代码恢复 NetBusinessServiceImpl
- 多进程 cgroup + iptables 网络流量监控
- System.out改为 SLF4J Logger
- @Service注解确保Spring自动装配
2026-07-22 17:32:55 +08:00
lee f1fa6dca7c fix: 恢复心跳发送、断线重连、注册定时任务的完整实现
故障诊断结果:Agent启动后TCP连接成功,但连接断开后无法重连

根因:
- connectionTask()重连方法是空stub,连接断开后无法自动重连
- heartbeatTask()心跳逻辑被简化,只是本地计数不通过TCP发送
- registerTask()注册方法是空stub
- MultiTargetNettyClient没有断线检测和重连机制

修复内容:
- BusinessTasks.java: 恢复完整的heartbeatTask(心跳上报)、connectionTask(断线重连)、
  registerTask(注册)、policyTask(策略更新)、networkDetectTask(多网IP探测)、
  checkFrpcTask(FRPC保活)、upFrpcMsgTask(FRPC状态上报)方法
- 新增 NetBusinessService.java 接口(定时任务依赖)
- 新增 NetBusinessVO.java VO类
- 新增 NetworkInterfaceInfo.java VO类
- 新增 NetworkInterfaceUtil.java 工具类
- MsgEnum.java: 新增心跳上报、注册、多网IP探测枚举值
2026-07-22 17:15:10 +08:00
lee ff7cd12fd7 fix: 恢复1.20版SaaS连接核心代码,修复Agent无法连接SaaS服务器问题
问题根因:1.21代码优化时误删了SaaS连接核心组件
- 误删 MultiTargetNettyClient.java (Netty TCP客户端)
- 误删 ConnectionConfig.java (连接配置封装)
- 误改 AgentNettyConfig 配置前缀从 netty.server 改为 tcp.netty.charge
- 误删 AppInitializer 中的SaaS连接初始化逻辑
- 误删 AgentService 中的 connection() 等接口方法
- 误增 AgentNettyServer.java (1.20中不存在的服务端类)

修复内容(从1.20原版JAR反编译恢复):
- 恢复 MultiTargetNettyClient: Bootstrap.connect() 主动连接SaaS服务器
- 恢复 ConnectionConfig: 连接配置封装类
- 恢复 AgentNettyConfig: @ConfigurationProperties(prefix = "netty.server")
- 恢复 AppInitializer: CommandLineRunner.run() 调用 initConnection()
- 恢复 AgentService/AgentServiceImpl: connection()方法及全部业务接口
- 恢复 MachineFingerprint: 硬件指纹生成工具类
- 恢复 application-dev.yml: netty.server.host/port 配置
- 删除 AgentNettyServer.java (1.21错误新增)
- 删除旧 AgentNettyConfig.java (tcp.netty.charge 前缀)

部署验证:39.96.218.1 已成功连接SaaS服务器 120.211.95.173:6620
2026-07-22 15:50:03 +08:00
lee 546eef3372 feat(v1.21-beta): 代码安全审计与全面优化
基线: v1.20 (e58137c) | 分支: agent1.21bate

## P0 严重修复 (14项)
- 安全: 命令注入漏洞修复 (SpecificTimeTaskService)
- 安全: 路径穿越漏洞修复 (AgentEndpoint)
- 安全: 下载安全加固 (AdvancedAsyncDownloader)
- 数据: CPU avg1/avg5/avg15 赋值错误修复 (CPUServiceImpl)
- 数据: 磁盘读写字节/速率赋值反转修复 (DiskServiceImpl)
- 逻辑: updateTaskInterval 丢失原始任务修复 (DynamicTaskService)
- 并发: GlobalConfig 100+ 静态字段 volatile + 并发集合
- 性能: SessionManager O(n)->O(1) Channel反向映射
- 并发: SessionManager 序列号 AtomicInteger
- 并发: CompensatedTrigger CAS 竞态修复
- 泄漏: UDPListenHandler ByteBuf 内存泄漏修复
- NPE: BaseNettyServer stop() 空指针修复
- 线程: AsyncCommandExecutor 无界线程池改有界+守护线程
- 泄漏: AdvancedAsyncDownloader 连接和线程池资源释放

## P1 重要修复 (5项)
- NetServiceImpl: Process 资源泄漏 + 超时控制
- DockerServiceImpl: 数组越界保护 + 资源释放
- MemoryServiceImpl: 除零风险修复
- SystemServiceImpl: 除零风险修复 + 异常处理规范化

## P2 代码质量 (6项)
- 100+ System.out/err -> AssertLog 日志框架
- e.printStackTrace() -> 结构化日志
- InterruptedException 中断状态恢复
- BaseNettyConfig Boolean 包装类型改基本类型
- BaseException cause 构造函数修复
- 新增安全配置项 (命令白名单/HMAC/下载安全/握手认证)

## 新增安全模块
- security/AuthHandshakeHandler.java - Netty握手认证
- security/HmacSignVerifier.java - HMAC签名验证
- security/SecureCommandExecutor.java - 安全命令执行器
- security/SecureFileDownloader.java - 安全文件下载器
- security/SecurityProperties.java - 安全配置属性
- security/SystemCommandRunner.java - 系统命令运行器
- 对应单元测试 4个

版本更新: pom.xml 0.0.1-SNAPSHOT -> 1.21-beta
          application.yml 1.0 -> 1.21

涉及文件: 34个 (21 modified + 13 new)
代码变更: +1085/-868 lines
2026-07-22 12:58:52 +08:00
53 changed files with 4523 additions and 1057 deletions
+47
View File
@@ -0,0 +1,47 @@
# tr-agent-client 版本记录
## 版本规则
- 主版本号.次版本号-阶段标识
- 阶段标识: beta(测试版) / rc(候选版) / release(正式版)
## 版本历史
### v1.21-beta (2026-07-22)
**基线版本**: v1.20 (commit: e58137c)
**分支**: agent1.21bate
**变更范围**: 代码安全审计与优化,涉及 21 个文件,+1085/-868 行
#### P0 严重修复 (安全 & 数据正确性)
1. **命令注入漏洞修复** - SpecificTimeTaskService: 添加命令安全校验、白名单机制、危险字符检测
2. **路径穿越漏洞修复** - AgentEndpoint: 添加 normalize() + startsWith() 路径校验
3. **下载安全加固** - AdvancedAsyncDownloader: 添加协议白名单、文件大小限制、路径穿越防护
4. **CPU负载数据丢失修复** - CPUServiceImpl: avg1/avg5/avg15 三行全调 setAvg1()1分钟和15分钟数据丢失
5. **磁盘读写数据反转修复** - DiskServiceImpl: 读写字节和读写速率赋值完全反转
6. **任务更新丢失修复** - DynamicTaskService: updateTaskInterval 用空 Runnable 替代了原始任务
7. **线程安全修复** - GlobalConfig: 100+ 静态字段添加 volatile,集合改为 ConcurrentHashMap/CopyOnWriteArrayList
8. **Session查找性能修复** - SessionManager: 新增 Channel->Session 反向映射,O(n)->O(1)
9. **序列号线程安全修复** - SessionManager: getSerialNumber 改用 AtomicInteger
10. **竞态条件修复** - CompensatedTrigger: get+set 竞态改用 CAS 循环
11. **内存泄漏修复** - UDPListenHandler: ByteBuf 未释放
12. **NPE修复** - BaseNettyServer: stop() 方法 NPE 风险
13. **无界线程池修复** - AsyncCommandExecutor: 改为有界线程池 + 守护线程
14. **资源泄漏修复** - AdvancedAsyncDownloader: 添加 connection.disconnect() 和线程池关闭
#### P1 重要修复 (资源管理 & 性能)
15. **进程资源泄漏修复** - NetServiceImpl: Process 对象未销毁,添加 destroy + waitFor 超时
16. **Docker资源泄漏修复** - DockerServiceImpl: 数组越界保护 + Process 资源释放
17. **除零风险修复** - MemoryServiceImpl: 内存总量为0时的除零异常
18. **除零风险修复** - SystemServiceImpl: 磁盘总量为0时的除零异常
19. **资源泄漏修复** - DockerServiceImpl/NetServiceImpl: Process/Reader 资源泄漏
#### P2 代码质量优化
20. **日志规范化** - 100+ 处 System.out/err 替换为 AssertLog 日志框架
21. **异常处理规范化** - 所有 e.printStackTrace() 替换为结构化日志
22. **中断处理规范化** - InterruptedException 添加中断状态恢复 (Thread.currentThread().interrupt())
23. **包装类型修复** - BaseNettyConfig: Boolean 包装类型改基本类型,消除 NPE 风险
24. **异常构造修复** - BaseException: cause 构造函数未设置 code/msg
25. **安全配置** - application-dev.yml: 新增安全配置项(命令白名单/HMAC签名/下载安全/握手认证)
### v1.20 (基线)
**commit**: e58137c "修正脚本执行策略上报信息"
**状态**: 优化前最后一个稳定版本,本次更新的基线
+1 -1
View File
@@ -10,7 +10,7 @@
</parent>
<groupId>com.tongran.agent</groupId>
<artifactId>tr-agent-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<version>1.21-beta</version>
<name>tr-agent-client</name>
<description>tr-agent-client</description>
@@ -9,6 +9,10 @@ public class ApplicationProperties {
private String name;
private String version;
private String confPath;
private String scriptPath;
private String tmpPath;
private String tempPath;
// Getter 和 Setter 方法
public String getName() {
@@ -26,4 +30,36 @@ public class ApplicationProperties {
public void setVersion(String version) {
this.version = version;
}
public String getConfPath() {
return confPath;
}
public void setConfPath(String confPath) {
this.confPath = confPath;
}
public String getScriptPath() {
return scriptPath;
}
public void setScriptPath(String scriptPath) {
this.scriptPath = scriptPath;
}
public String getTmpPath() {
return tmpPath;
}
public void setTmpPath(String tmpPath) {
this.tmpPath = tmpPath;
}
public String getTempPath() {
return tempPath;
}
public void setTempPath(String tempPath) {
this.tempPath = tempPath;
}
}
@@ -3,14 +3,15 @@ package com.tongran.agent.client.core.config;
import com.tongran.agent.client.core.eo.AlarmEO;
import com.tongran.agent.client.core.eo.NativeNetworkInterfaceEO;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 全局配置类
* 注意:所有可变集合均使用线程安全实现,基本类型使用 volatile 保证可见性
*/
public class GlobalConfig {
@@ -18,175 +19,181 @@ public class GlobalConfig {
/**
* 服务启动时间
*/
public static long startupTime;
public static volatile long startupTime;
/**
* 客户端ID - 对应平台唯一SN
*/
public static String CLIENT_ID;
public static volatile String CLIENT_ID;
/** 设备序列号 */
public static volatile String DEVICE_SN;
/** 是否已注册 */
public static volatile boolean isRegister = false;
/**
* 交换机信息
*/
public static String SWITCH_COMMUNITY; //团名
public static String SWITCH_IP; //交换机IP
public static int SWITCH_PORT; //交换机端口
public static LinkedHashMap<String, String> SWITCH_NET_OID = new LinkedHashMap<>(); //交换机网络端口发现OID
public static LinkedHashMap<String, String> SWITCH_MODULE_OID = new LinkedHashMap<>(); //交换机光模块发现OID
public static LinkedHashMap<String, String> SWITCH_MPU_OID = new LinkedHashMap<>(); //交换机MPU发现OID
public static LinkedHashMap<String, String> SWITCH_PWR_OID = new LinkedHashMap<>(); //交换机电源发现OID
public static LinkedHashMap<String, String> SWITCH_FAN_OID = new LinkedHashMap<>(); //交换机风扇发现OID
public static LinkedHashMap<String, String> SWITCH_OTHER_OID = new LinkedHashMap<>(); //交换机系统其他OID
public static String OTHER_INDEX_PARAM = "entIndex"; //交换机其他监控项索引参数
public static String OTHER_INDEX_OID = "";
public static List<String> OTHER_FILTER = new ArrayList<>(); //过滤值
public static String NET_INDEX_PARAM = "ifIndex"; //交换机网络端口索引参数
public static String NET_INDEX_OID = "";
public static List<String> NET_FILTER = new ArrayList<>(); //过滤值
public static String MODULE_INDEX_PARAM = "fiberEntIndex"; //交换机光模块端口索引参数
public static String MODULE_INDEX_OID = "";
public static List<String> MODULE_FILTER = new ArrayList<>(); //过滤值
public static String MPU_INDEX_PARAM = "mpuEntIndex"; //交换机MPU索引参数
public static String MPU_INDEX_OID = "";
public static List<String> MPU_FILTER = new ArrayList<>(); //过滤值
public static String PWR_INDEX_PARAM = "pwrEntIndex"; //交换机电源索引参数
public static String PWR_INDEX_OID = "";
public static List<String> PWR_FILTER = new ArrayList<>(); //过滤值
public static String FAN_INDEX_PARAM = "fanEntIndex"; //交换机风扇索引参数
public static String FAN_INDEX_OID = "";
public static List<String> FAN_FILTER = new ArrayList<>(); //过滤值
public static volatile String SWITCH_COMMUNITY; //团名
public static volatile String SWITCH_IP; //交换机IP
public static volatile int SWITCH_PORT; //交换机端口
public static final Map<String, String> SWITCH_NET_OID = new ConcurrentHashMap<>(); //交换机网络端口发现OID
public static final Map<String, String> SWITCH_MODULE_OID = new ConcurrentHashMap<>(); //交换机光模块发现OID
public static final Map<String, String> SWITCH_MPU_OID = new ConcurrentHashMap<>(); //交换机MPU发现OID
public static final Map<String, String> SWITCH_PWR_OID = new ConcurrentHashMap<>(); //交换机电源发现OID
public static final Map<String, String> SWITCH_FAN_OID = new ConcurrentHashMap<>(); //交换机风扇发现OID
public static final Map<String, String> SWITCH_OTHER_OID = new ConcurrentHashMap<>(); //交换机系统其他OID
public static volatile String OTHER_INDEX_PARAM = "entIndex"; //交换机其他监控项索引参数
public static volatile String OTHER_INDEX_OID = "";
public static final List<String> OTHER_FILTER = new CopyOnWriteArrayList<>(); //过滤值
public static volatile String NET_INDEX_PARAM = "ifIndex"; //交换机网络端口索引参数
public static volatile String NET_INDEX_OID = "";
public static final List<String> NET_FILTER = new CopyOnWriteArrayList<>(); //过滤值
public static volatile String MODULE_INDEX_PARAM = "fiberEntIndex"; //交换机光模块端口索引参数
public static volatile String MODULE_INDEX_OID = "";
public static final List<String> MODULE_FILTER = new CopyOnWriteArrayList<>(); //过滤值
public static volatile String MPU_INDEX_PARAM = "mpuEntIndex"; //交换机MPU索引参数
public static volatile String MPU_INDEX_OID = "";
public static final List<String> MPU_FILTER = new CopyOnWriteArrayList<>(); //过滤值
public static volatile String PWR_INDEX_PARAM = "pwrEntIndex"; //交换机电源索引参数
public static volatile String PWR_INDEX_OID = "";
public static final List<String> PWR_FILTER = new CopyOnWriteArrayList<>(); //过滤值
public static volatile String FAN_INDEX_PARAM = "fanEntIndex"; //交换机风扇索引参数
public static volatile String FAN_INDEX_OID = "";
public static final List<String> FAN_FILTER = new CopyOnWriteArrayList<>(); //过滤值
/**
* 采集标识
*/
public static boolean isCollect = false;
public static Map<String, Long> taskIds = new ConcurrentHashMap<>();
public static volatile boolean isCollect = false;
public static final Map<String, Long> taskIds = new ConcurrentHashMap<>();
/**
* 采集系统监控
*/
public static boolean cpuCollect = false; //cpu采集
public static long cpuInterval = 300; //cpu采集间隔
public static boolean vfsCollect = false; //挂载采集
public static long vfsInterval = 300; //挂载采集间隔
public static boolean netCollect = false; //网络采集
public static long netInterval = 300; //网络采集间隔
public static boolean diskCollect = false; //硬盘采集
public static long diskInterval = 300; //硬盘采集间隔
public static boolean dockerCollect = false; //docker采集
public static long dockerInterval = 300; //docker采集间隔
public static volatile boolean cpuCollect = false; //cpu采集
public static volatile long cpuInterval = 300; //cpu采集间隔
public static volatile boolean vfsCollect = false; //挂载采集
public static volatile long vfsInterval = 300; //挂载采集间隔
public static volatile boolean netCollect = false; //网络采集
public static volatile long netInterval = 300; //网络采集间隔
public static volatile boolean diskCollect = false; //硬盘采集
public static volatile long diskInterval = 300; //硬盘采集间隔
public static volatile boolean dockerCollect = false; //docker采集
public static volatile long dockerInterval = 300; //docker采集间隔
/**
* 采集系统其他监控
*/
public static boolean systemSwapSizeFreeCollect = false; //交换卷/文件的可用空间(字节)采集
public static long systemSwapSizeFreeInterval = 300; //交换卷/文件的可用空间(字节)采集间隔
public static boolean memoryUtilizationCollect = false; //内存利用率采集
public static long memoryUtilizationInterval = 300; //内存利用率采集间隔
public static boolean systemSwapSizePercentCollect = false; //可用交换空间百分比采集
public static long systemSwapSizePercentInterval = 300; //可用交换空间百分比采集间隔
public static boolean memorySizeAvailableCollect = false; //可用内存采集
public static long memorySizeAvailableInterval = 300; //可用内存采集间隔
public static boolean memorySizePercentCollect = false; //可用内存百分比采集
public static long memorySizePercentInterval = 300; //可用内存百分比采集间隔
public static boolean memorySizeTotalCollect = false; //总内存采集
public static long memorySizeTotalInterval = 300; //总内存采集间隔
public static boolean systemSwOsCollect = false; //操作系统采集
public static long systemSwOsInterval = 300; //操作系统采集间隔
public static boolean systemSwArchCollect = false; //操作系统架构采集
public static long systemSwArchInterval = 300; //操作系统架构采集间隔
public static boolean kernelMaxprocCollect = false; //最大进程数采集
public static long kernelMaxprocInterval = 300; //最大进程数采集间隔
public static boolean procNumRunCollect = false; //正在运行的进程数采集
public static long procNumRunInterval = 300; //正在运行的进程数采集间隔
public static boolean systemUsersNumCollect = false; //登录用户数采集
public static long systemUsersNumInterval = 300; //登录用户数采集间隔
public static boolean systemDiskSizeTotalCollect = false; ///硬盘总可用空间采集
public static long systemDiskSizeTotalInterval = 300; //硬盘总可用空间采集间隔
public static boolean systemBoottimeCollect = false; //系统启动时间采集
public static long systemBoottimeInterval = 300; //系统启动时间采集间隔
public static boolean systemUnameCollect = false; //系统描述采集
public static long systemUnameInterval = 300; //系统描述采集间隔
public static boolean systemLocaltimeCollect = false; //系统本地时间采集
public static long systemLocaltimeInterval = 300; //系统本地时间采集间隔
public static boolean systemUptimeCollect = false; //系统正常运行时间采集
public static long systemUptimeInterval = 300; //系统正常运行时间采集间隔
public static boolean procNumCollect = false; //进程数采集
public static long procNumInterval = 300; //进程数采集间隔
public static volatile boolean systemSwapSizeFreeCollect = false; //交换卷/文件的可用空间(字节)采集
public static volatile long systemSwapSizeFreeInterval = 300; //交换卷/文件的可用空间(字节)采集间隔
public static volatile boolean memoryUtilizationCollect = false; //内存利用率采集
public static volatile long memoryUtilizationInterval = 300; //内存利用率采集间隔
public static volatile boolean systemSwapSizePercentCollect = false; //可用交换空间百分比采集
public static volatile long systemSwapSizePercentInterval = 300; //可用交换空间百分比采集间隔
public static volatile boolean memorySizeAvailableCollect = false; //可用内存采集
public static volatile long memorySizeAvailableInterval = 300; //可用内存采集间隔
public static volatile boolean memorySizePercentCollect = false; //可用内存百分比采集
public static volatile long memorySizePercentInterval = 300; //可用内存百分比采集间隔
public static volatile boolean memorySizeTotalCollect = false; //总内存采集
public static volatile long memorySizeTotalInterval = 300; //总内存采集间隔
public static volatile boolean systemSwOsCollect = false; //操作系统采集
public static volatile long systemSwOsInterval = 300; //操作系统采集间隔
public static volatile boolean systemSwArchCollect = false; //操作系统架构采集
public static volatile long systemSwArchInterval = 300; //操作系统架构采集间隔
public static volatile boolean kernelMaxprocCollect = false; //最大进程数采集
public static volatile long kernelMaxprocInterval = 300; //最大进程数采集间隔
public static volatile boolean procNumRunCollect = false; //正在运行的进程数采集
public static volatile long procNumRunInterval = 300; //正在运行的进程数采集间隔
public static volatile boolean systemUsersNumCollect = false; //登录用户数采集
public static volatile long systemUsersNumInterval = 300; //登录用户数采集间隔
public static volatile boolean systemDiskSizeTotalCollect = false; ///硬盘总可用空间采集
public static volatile long systemDiskSizeTotalInterval = 300; //硬盘总可用空间采集间隔
public static volatile boolean systemBoottimeCollect = false; //系统启动时间采集
public static volatile long systemBoottimeInterval = 300; //系统启动时间采集间隔
public static volatile boolean systemUnameCollect = false; //系统描述采集
public static volatile long systemUnameInterval = 300; //系统描述采集间隔
public static volatile boolean systemLocaltimeCollect = false; //系统本地时间采集
public static volatile long systemLocaltimeInterval = 300; //系统本地时间采集间隔
public static volatile boolean systemUptimeCollect = false; //系统正常运行时间采集
public static volatile long systemUptimeInterval = 300; //系统正常运行时间采集间隔
public static volatile boolean procNumCollect = false; //进程数采集
public static volatile long procNumInterval = 300; //进程数采集间隔
/**
* 采集交换机监控
*/
public static boolean switchNetCollect = false; //交换机网络采集
public static long switchNetInterval = 300; //交换机网络采集间隔
public static boolean switchModuleCollect = false; //光模块采集
public static long switchModuleInterval = 300; //光模块采集间隔
public static boolean switchMpuCollect = false; //MPU采集
public static long switchMpuInterval = 300; //MPU采集间隔
public static boolean switchPwrCollect = false; //电源采集
public static long switchPwrInterval = 300; //电源采集间隔
public static boolean switchFanCollect = false; //风扇采集
public static long switchFanInterval = 300; //风扇采集间隔
public static volatile boolean switchNetCollect = false; //交换机网络采集
public static volatile long switchNetInterval = 300; //交换机网络采集间隔
public static volatile boolean switchModuleCollect = false; //光模块采集
public static volatile long switchModuleInterval = 300; //光模块采集间隔
public static volatile boolean switchMpuCollect = false; //MPU采集
public static volatile long switchMpuInterval = 300; //MPU采集间隔
public static volatile boolean switchPwrCollect = false; //电源采集
public static volatile long switchPwrInterval = 300; //电源采集间隔
public static volatile boolean switchFanCollect = false; //风扇采集
public static volatile long switchFanInterval = 300; //风扇采集间隔
/**
* 采集交换机其他监控
*/
public static boolean switchSysDescrCollect = false; //系统描述采集
public static long switchSysDescrInterval = 300; //系统描述采集间隔
public static boolean switchSysObjectIDCollect = false; //系统Object ID采集
public static long switchSysObjectIDInterval = 300; //系统Object ID采集间隔
public static boolean switchSysUpTimeCollect = false; //系统运行时间采集
public static long switchSysUpTimeInterval = 300; //系统运行时间采集间隔
public static boolean switchSysContactCollect = false; //系统联系信息采集
public static long switchSysContactInterval = 300; //系统联系信息采集间隔
public static boolean switchSysNameCollect = false; //系统名称采集
public static long switchSysNameInterval = 300; //系统名称采集间隔
public static boolean switchSysLocationCollect = false; //系统位置采集
public static long switchSysLocationInterval = 300; //系统位置采集间隔
public static boolean switchHwStackSystemMacCollect = false; //系统MAC地址采集
public static long switchHwStackSystemMacInterval = 300; //系统MAC地址采集间隔
public static boolean switchEntIndexCollect = false; //设备索引采集
public static long switchEntIndexInterval = 300; //设备索引采集间隔
public static boolean switchEntPhysicalNameCollect = false; //设备名称采集
public static long switchEntPhysicalNameInterval = 300; //设备名称采集间隔
public static boolean switchEntPhysicalSoftwareRevCollect = false; //设备软件版本采集
public static long switchEntPhysicalSoftwareRevInterval = 300; //设备软件版本采集间隔
public static boolean switchHwEntityCpuUsageCollect = false; //设备CPU使用率(%)采集
public static long switchHwEntityCpuUsageInterval = 300; //设备CPU使用率(%)采集间隔
public static boolean switchHwEntityMemUsageCollect = false; //设备内存使用率(%)采集
public static long switchHwEntityMemUsageInterval = 300; //设备内存使用率(%)采集间隔
public static boolean switchHwAveragePowerCollect = false; //系统平均功率(mW)采集
public static long switchHwAveragePowerInterval = 300; //系统平均功率(mW)采集间隔
public static boolean switchHwCurrentPowerCollect = false; //系统实时功率(mW)采集
public static long switchHwCurrentPowerInterval = 300; //系统实时功率(mW)采集间隔
public static volatile boolean switchSysDescrCollect = false; //系统描述采集
public static volatile long switchSysDescrInterval = 300; //系统描述采集间隔
public static volatile boolean switchSysObjectIDCollect = false; //系统Object ID采集
public static volatile long switchSysObjectIDInterval = 300; //系统Object ID采集间隔
public static volatile boolean switchSysUpTimeCollect = false; //系统运行时间采集
public static volatile long switchSysUpTimeInterval = 300; //系统运行时间采集间隔
public static volatile boolean switchSysContactCollect = false; //系统联系信息采集
public static volatile long switchSysContactInterval = 300; //系统联系信息采集间隔
public static volatile boolean switchSysNameCollect = false; //系统名称采集
public static volatile long switchSysNameInterval = 300; //系统名称采集间隔
public static volatile boolean switchSysLocationCollect = false; //系统位置采集
public static volatile long switchSysLocationInterval = 300; //系统位置采集间隔
public static volatile boolean switchHwStackSystemMacCollect = false; //系统MAC地址采集
public static volatile long switchHwStackSystemMacInterval = 300; //系统MAC地址采集间隔
public static volatile boolean switchEntIndexCollect = false; //设备索引采集
public static volatile long switchEntIndexInterval = 300; //设备索引采集间隔
public static volatile boolean switchEntPhysicalNameCollect = false; //设备名称采集
public static volatile long switchEntPhysicalNameInterval = 300; //设备名称采集间隔
public static volatile boolean switchEntPhysicalSoftwareRevCollect = false; //设备软件版本采集
public static volatile long switchEntPhysicalSoftwareRevInterval = 300; //设备软件版本采集间隔
public static volatile boolean switchHwEntityCpuUsageCollect = false; //设备CPU使用率(%)采集
public static volatile long switchHwEntityCpuUsageInterval = 300; //设备CPU使用率(%)采集间隔
public static volatile boolean switchHwEntityMemUsageCollect = false; //设备内存使用率(%)采集
public static volatile long switchHwEntityMemUsageInterval = 300; //设备内存使用率(%)采集间隔
public static volatile boolean switchHwAveragePowerCollect = false; //系统平均功率(mW)采集
public static volatile long switchHwAveragePowerInterval = 300; //系统平均功率(mW)采集间隔
public static volatile boolean switchHwCurrentPowerCollect = false; //系统实时功率(mW)采集
public static volatile long switchHwCurrentPowerInterval = 300; //系统实时功率(mW)采集间隔
/**
* 告警
*/
public static boolean IS_ALARM = false;
public static long ALARM_INTERVAL = 60;
public static List<AlarmEO> ALARM_LIST = new ArrayList<>(); //告警设置信息
public static volatile boolean IS_ALARM = false;
public static volatile long ALARM_INTERVAL = 60;
public static final List<AlarmEO> ALARM_LIST = new CopyOnWriteArrayList<>(); //告警设置信息
/**
* 告警监控
*/
public static boolean systemCpuUti = false; //CPU使用率
public static boolean memoryUtilization = false; //内存利用率
public static boolean systemSwapSizePercent = false; //可用交换空间百分比
public static boolean systemUsersNum = false; //登录用户数
public static boolean vfsFsUtil = false; //挂载点的空间利用率
public static boolean netIfStatus = false; //网络运行状态UP变down
public static boolean netUtil = false; //网络带宽使用率
public static boolean containerMemUtil = false; //容器内存使用率
public static boolean extraPorts = false; //多余端口
public static volatile boolean systemCpuUti = false; //CPU使用率
public static volatile boolean memoryUtilization = false; //内存利用率
public static volatile boolean systemSwapSizePercent = false; //可用交换空间百分比
public static volatile boolean systemUsersNum = false; //登录用户数
public static volatile boolean vfsFsUtil = false; //挂载点的空间利用率
public static volatile boolean netIfStatus = false; //网络运行状态UP变down
public static volatile boolean netUtil = false; //网络带宽使用率
public static volatile boolean containerMemUtil = false; //容器内存使用率
public static volatile boolean extraPorts = false; //多余端口
/**
* 所有网络接口
*/
public static List<NativeNetworkInterfaceEO> NET_LIST = new ArrayList<>();
public static final List<NativeNetworkInterfaceEO> NET_LIST = new CopyOnWriteArrayList<>();
/**
* 脚本文件下载标识
*/
public static LinkedHashMap<String, Integer> DOWN_FILES = new LinkedHashMap<>();
public static final Map<String, AtomicInteger> DOWN_FILES = new ConcurrentHashMap<>();
}
@@ -40,6 +40,16 @@ public enum MsgEnum {
告警上报("ALARM"),
建立连接("CONNECT"),
获取最新策略("GET_POLICY"),
多网IP探测上报("NETWORK_DETECT"),
内存详情上报("MEMORY_DETAIL"),
业务网络上报("BUSINESS_NET"),
开启或更新系统采集("SYSTEM_COLLECT_START"),
开启或更新系统采集应答("SYSTEM_COLLECT_START_RSP"),
@@ -16,6 +16,7 @@ import org.apache.commons.lang3.StringUtils;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Data
public class SessionManager {
@@ -25,8 +26,14 @@ public class SessionManager {
// clientId-Session
private final Map<String, Session> sessionMap = new ConcurrentHashMap<>();
// Channel-Session 反向映射,避免 O(n) 遍历查找
private final Map<Channel, String> channelToClientId = new ConcurrentHashMap<>();
private final Cache<String, Object> sessionCache = CacheUtil.newTimedCache(10 * 60 * 1000);
// 流水号生成器,使用 AtomicInteger 保证线程安全
private final AtomicInteger serialNumberGenerator = new AtomicInteger(0);
public static SessionManager getInstance() {
if (instance == null) {
synchronized (SessionManager.class) {
@@ -38,40 +45,54 @@ public class SessionManager {
return instance;
}
public synchronized void put(String clientId, Session session) {
public void put(String clientId, Session session) {
if (StringUtils.isNotBlank(session.getClientId())) {
sessionMap.put(session.getClientId(), session);
// 维护反向映射
if (session.getChannel() != null) {
channelToClientId.put(session.getChannel().channel(), session.getClientId());
}
}
}
public synchronized void remove(String clientId) {
public void remove(String clientId) {
if (StringUtils.isNotBlank(clientId)) {
sessionMap.remove(clientId);
Session removed = sessionMap.remove(clientId);
if (removed != null && removed.getChannel() != null) {
channelToClientId.remove(removed.getChannel().channel());
}
}
}
public synchronized void remove(Session session) {
public void remove(Session session) {
if (session != null && StringUtils.isNotBlank(session.getClientId())) {
sessionMap.remove(session.getClientId(), session);
if (session.getChannel() != null) {
channelToClientId.remove(session.getChannel().channel());
}
}
}
public synchronized void remove(Channel channel) {
remove(getSessionByChannel(channel));
public void remove(Channel channel) {
String clientId = channelToClientId.remove(channel);
if (clientId != null) {
sessionMap.remove(clientId);
}
}
public Session getSessionById(String clientId) {
return sessionMap.get(clientId);
}
/**
* 通过 Channel 查找 Session,使用反向映射表 O(1) 查找
*/
public Session getSessionByChannel(Channel channel) {
Session session = new Session();
sessionMap.values().forEach(s -> {
if (s.getChannel().channel() == channel) {
BeanUtil.copyProperties(s, session, true);
}
});
return session;
String clientId = channelToClientId.get(channel);
if (clientId != null) {
return sessionMap.get(clientId);
}
return null;
}
public boolean containsSession(String clientId) {
@@ -100,21 +121,12 @@ public class SessionManager {
}
/**
* 根据channel生成流水号
*
* @param channel
* @return
* 根据channel生成流水号,使用 AtomicInteger 保证线程安全
*/
public short getSerialNumber(Channel channel, AttributeKey<Short> serialNumber) {
Attribute<Short> flowIdAttr = channel.attr(serialNumber);
Short flowId = flowIdAttr.get();
if (flowId == null) {
flowId = 0;
} else {
flowId++;
}
flowIdAttr.set(flowId);
return flowId;
int next = serialNumberGenerator.incrementAndGet();
// short 类型溢出后回绕到 0
return (short) (next & 0xFFFF);
}
public void writeAndFlush(String clientId, Object msg) {
@@ -134,4 +146,4 @@ public class SessionManager {
});
}
}
}
@@ -0,0 +1,13 @@
package com.tongran.agent.client.core.vo;
import lombok.Data;
/**
* MacVlan状态VO
*/
@Data
public class MacVlanVO {
private String vlanId;
private String mid;
private String status;
}
@@ -0,0 +1,27 @@
package com.tongran.agent.client.core.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class NetBusinessVO implements Serializable {
private static final long serialVersionUID = 9L;
private String name;
private String mac;
private int pid;
private String processName;
private Long inSpeed;
private Long outSpeed;
private Long ipv4InSpeed;
private Long ipv4OutSpeed;
private Long ipv6InSpeed;
private Long ipv6OutSpeed;
private int connectionCount;
private long timestamp;
}
@@ -0,0 +1,74 @@
package com.tongran.agent.client.core.vo;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.util.List;
/**
* 网络接口信息VO(从1.20恢复)
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class NetworkInterfaceInfo {
private String name;
private String mac;
private String type;
private String ipv4;
private String gateway;
private String publicIp;
private String carrier;
private String province;
private String city;
private String ipv6;
private String status;
private String parentInterface;
private Long netCreateTime;
private List<NetworkInterfaceInfo> subInterfaces;
/**
* 兼容旧builder模式
*/
public static NetworkInterfaceInfoBuilder builder() {
return new NetworkInterfaceInfoBuilder();
}
public static class NetworkInterfaceInfoBuilder {
private String name;
private String mac;
private String type;
private String ipv4;
private String gateway;
private String publicIp;
private String carrier;
private String province;
private String city;
private String ipv6;
private String status;
private String parentInterface;
private Long netCreateTime;
private List<NetworkInterfaceInfo> subInterfaces;
public NetworkInterfaceInfoBuilder name(String name) { this.name = name; return this; }
public NetworkInterfaceInfoBuilder mac(String mac) { this.mac = mac; return this; }
public NetworkInterfaceInfoBuilder type(String type) { this.type = type; return this; }
public NetworkInterfaceInfoBuilder ipv4(String ipv4) { this.ipv4 = ipv4; return this; }
public NetworkInterfaceInfoBuilder gateway(String gateway) { this.gateway = gateway; return this; }
public NetworkInterfaceInfoBuilder publicIp(String publicIp) { this.publicIp = publicIp; return this; }
public NetworkInterfaceInfoBuilder carrier(String carrier) { this.carrier = carrier; return this; }
public NetworkInterfaceInfoBuilder province(String province) { this.province = province; return this; }
public NetworkInterfaceInfoBuilder city(String city) { this.city = city; return this; }
public NetworkInterfaceInfoBuilder ipv6(String ipv6) { this.ipv6 = ipv6; return this; }
public NetworkInterfaceInfoBuilder status(String status) { this.status = status; return this; }
public NetworkInterfaceInfoBuilder parentInterface(String parentInterface) { this.parentInterface = parentInterface; return this; }
public NetworkInterfaceInfoBuilder netCreateTime(Long netCreateTime) { this.netCreateTime = netCreateTime; return this; }
public NetworkInterfaceInfoBuilder subInterfaces(List<NetworkInterfaceInfo> subInterfaces) { this.subInterfaces = subInterfaces; return this; }
public NetworkInterfaceInfo build() {
return new NetworkInterfaceInfo(name, mac, type, ipv4, gateway, publicIp, carrier, province, city, ipv6, status, parentInterface, netCreateTime, subInterfaces);
}
}
}
@@ -16,6 +16,7 @@ public class BaseException extends RuntimeException implements ErrorCode {
public BaseException(String msg, Throwable cause) {
super(msg, cause);
this.msg = msg; // 修复:设置 msg 字段
}
public BaseException(Integer code, String msg) {
@@ -39,4 +40,4 @@ public class BaseException extends RuntimeException implements ErrorCode {
public String getMsg() {
return msg;
}
}
}
@@ -1,22 +0,0 @@
package com.tongran.agent.client.netty;
import com.tongran.agent.client.netty.config.BaseNettyConfig;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* Netty配置
*/
@Data
@Configuration
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@ConfigurationProperties(prefix = "tcp.netty.charge")
public class AgentNettyConfig extends BaseNettyConfig {
private static final long serialVersionUID = -4284214721383107291L;
}
@@ -1,57 +0,0 @@
package com.tongran.agent.client.netty;
import com.tongran.agent.client.netty.handler.AgentDecoderHandler;
import com.tongran.agent.client.netty.handler.AgentDispatcherHandler;
import com.tongran.agent.client.netty.handler.AgentEncoderHandler;
import com.tongran.agent.client.netty.handler.TCPListenHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
@Configuration
public class AgentNettyServer extends BaseNettyServer {
@Resource
private AgentNettyConfig config;
@Resource
private TCPListenHandler tcpListenHandler;
@Resource
private AgentDecoderHandler decoderHandler;
@Resource
private AgentEncoderHandler encoderHandler;
@Resource
private AgentDispatcherHandler dispatcherHandler;
protected AgentNettyServer(AgentNettyConfig config) {
super(config);
}
@Bean(name = "NettyCharge", initMethod = "start", destroyMethod = "stop")
BaseNettyServer run() {
config.setHander(new ChannelInitializer<NioSocketChannel>() {
@Override
public void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new IdleStateHandler(config.readerIdleTime, config.writerIdleTime, config.allIdleTime));// 心跳
ch.pipeline().addLast(tcpListenHandler);// 监听器
//入栈
ch.pipeline().addLast(decoderHandler);//解码器
//出栈
ch.pipeline().addLast(encoderHandler);//加码器
// 业务分发是入站最后一个处理器, 出站第一个处理器, 位置要放在最后
ch.pipeline().addLast(businessGroup, dispatcherHandler);//业务分发 这里使用业务线程组
}
});
return new AgentNettyServer(config);
}
}
@@ -97,20 +97,28 @@ public abstract class BaseNettyServer {
}
isRunning = false;
try {
Future<?> future = this.workerGroup.shutdownGracefully().await();
if (!future.isSuccess()) {
AssertLog.error("workerGroup 无法正常停止:{}", future.cause());
// 修复:每个 group 判空后再关闭
if (this.workerGroup != null) {
Future<?> future = this.workerGroup.shutdownGracefully().await();
if (!future.isSuccess()) {
AssertLog.error("workerGroup 无法正常停止:{}", future.cause());
}
}
future = this.bossGroup.shutdownGracefully().await();
if (!future.isSuccess()) {
AssertLog.error("bossGroup 无法正常停止:{}", future.cause());
if (this.bossGroup != null) {
Future<?> future = this.bossGroup.shutdownGracefully().await();
if (!future.isSuccess()) {
AssertLog.error("bossGroup 无法正常停止:{}", future.cause());
}
}
future = this.businessGroup.shutdownGracefully().await();
if (!future.isSuccess()) {
AssertLog.error("businessGroup 无法正常停止:{}", future.cause());
if (this.businessGroup != null) {
Future<?> future = this.businessGroup.shutdownGracefully().await();
if (!future.isSuccess()) {
AssertLog.error("businessGroup 无法正常停止:{}", future.cause());
}
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
AssertLog.error("Netty服务停止被中断", e);
}
AssertLog.info("\n\n\t\t\t\t\t\t\t\t======{} 已经停止,port:{}======\n", config.name, config.port);
}
@@ -0,0 +1,229 @@
package com.tongran.agent.client.netty;
import com.tongran.agent.client.netty.handler.AgentDecoderHandler;
import com.tongran.agent.client.netty.handler.AgentDispatcherHandler;
import com.tongran.agent.client.netty.handler.AgentEncoderHandler;
import com.tongran.agent.client.netty.model.Message;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.AttributeKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* 多目标Netty客户端
* 负责主动连接SaaS服务器,维护长连接通道
*/
@Component
public class MultiTargetNettyClient {
private static final Logger logger = LoggerFactory.getLogger(MultiTargetNettyClient.class);
private static final AttributeKey<String> CONNECTION_KEY = AttributeKey.valueOf("connectionKey");
/** 连接映射表: connectionKey -> Channel */
private final Map<String, Channel> connectionMap = new ConcurrentHashMap<>();
/** 反向映射表: Channel -> connectionKey */
private final Map<Channel, String> reverseConnectionMap = new ConcurrentHashMap<>();
private EventLoopGroup workerGroup;
@Resource
private AgentDecoderHandler decoderHandler;
@Resource
private AgentEncoderHandler encoderHandler;
@Resource
private AgentDispatcherHandler dispatcherHandler;
/**
* 创建到目标服务器的连接
*
* @param connectionKey 连接标识
* @param host 目标主机
* @param port 目标端口
* @param timeout 连接超时(秒)
* @return 连接是否成功
*/
public boolean createConnection(String connectionKey, String host, int port, int timeout) {
if (this.workerGroup == null) {
this.workerGroup = new NioEventLoopGroup();
}
if (this.connectionMap.containsKey(connectionKey)) {
logger.info("连接已存在: {}", connectionKey);
return true;
}
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(this.workerGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, timeout * 1000)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(decoderHandler);
pipeline.addLast(encoderHandler);
pipeline.addLast(dispatcherHandler);
}
});
try {
CountDownLatch latch = new CountDownLatch(1);
boolean[] success = {false};
bootstrap.connect(host, port).addListener((ChannelFuture future) -> {
if (future.isSuccess()) {
Channel channel = future.channel();
connectionMap.put(connectionKey, channel);
reverseConnectionMap.put(channel, connectionKey);
success[0] = true;
logger.info("连接建立成功: {} -> {}:{}", connectionKey, host, port);
} else {
logger.error("连接建立失败: {} - {}", connectionKey, future.cause().getMessage());
success[0] = false;
}
latch.countDown();
});
return latch.await(timeout, TimeUnit.SECONDS) && success[0];
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.error("连接建立被中断: {}", connectionKey, e);
return false;
}
}
/**
* 发送字符串消息
*/
public boolean sendMessage(String connectionKey, String message) {
Channel channel = this.connectionMap.get(connectionKey);
if (channel == null) {
logger.error("连接不存在: {}", connectionKey);
return false;
}
if (!channel.isActive()) {
logger.error("连接已断开: {}", connectionKey);
this.removeConnection(connectionKey);
return false;
}
try {
ChannelFuture future = channel.writeAndFlush(message).sync();
logger.info("消息发送成功到: {} - {}", connectionKey, message);
return future.isSuccess();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
/**
* 发送Message对象消息
*/
public boolean sendMessages(String connectionKey, Message message) {
Channel channel = this.connectionMap.get(connectionKey);
if (channel == null) {
logger.error("连接不存在: {}", connectionKey);
return false;
}
if (!channel.isActive()) {
logger.error("连接已断开: {}", connectionKey);
this.removeConnection(connectionKey);
return false;
}
try {
ChannelFuture future = channel.writeAndFlush(message).sync();
logger.info("消息发送成功到: {} - {}", connectionKey, message);
return future.isSuccess();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
/**
* 批量发送消息
*/
public void sendMessages(Map<String, String> messages) {
messages.forEach((connectionKey, message) -> new Thread(() -> {
if (this.sendMessage(connectionKey, message)) {
logger.info("批量发送成功: {}", connectionKey);
} else {
logger.error("批量发送失败: {}", connectionKey);
}
}).start());
}
/**
* 关闭指定连接
*/
public void closeConnection(String connectionKey) {
Channel channel = this.connectionMap.get(connectionKey);
if (channel != null) {
channel.close();
this.removeConnection(connectionKey);
logger.info("连接已关闭: {}", connectionKey);
}
}
/**
* 移除连接记录
*/
private void removeConnection(String connectionKey) {
Channel channel = this.connectionMap.remove(connectionKey);
if (channel != null) {
this.reverseConnectionMap.remove(channel);
}
}
/**
* 关闭所有连接并释放资源
*/
public void shutdown() {
this.connectionMap.forEach((key, channel) -> {
if (channel.isActive()) {
channel.close();
}
});
this.connectionMap.clear();
this.reverseConnectionMap.clear();
if (this.workerGroup != null) {
this.workerGroup.shutdownGracefully();
}
logger.info("所有连接已关闭");
}
/**
* 获取活跃连接数
*/
public int getActiveConnections() {
return (int) this.connectionMap.values().stream().filter(Channel::isActive).count();
}
/**
* 获取指定连接的Channel
*/
public Channel get(String connectionKey) {
return this.connectionMap.get(connectionKey);
}
}
@@ -0,0 +1,20 @@
package com.tongran.agent.client.netty.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* Netty服务器连接配置
* 读取 netty.server.host 和 netty.server.port 配置
* 用于Agent客户端连接SaaS服务器
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "netty.server")
public class AgentNettyConfig {
private String host;
private int port;
}
@@ -18,12 +18,12 @@ public class BaseNettyConfig implements Serializable {
/**
* 是否开启
*/
public Boolean enable = false;
public boolean enable = false;
/**
* 是否TCP
*/
public Boolean isTcp = true;
public boolean isTcp = true;
/**
* 服务名称
@@ -0,0 +1,50 @@
package com.tongran.agent.client.netty.config;
import java.util.Objects;
/**
* 连接配置类
* 封装单个Netty连接的配置信息
*/
public class ConnectionConfig {
private final String connectionKey;
private final String host;
private final int port;
private final int timeout;
public ConnectionConfig(String connectionKey, String host, int port, int timeout) {
this.connectionKey = connectionKey;
this.host = host;
this.port = port;
this.timeout = timeout;
}
public String getConnectionKey() {
return connectionKey;
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
public int getTimeout() {
return timeout;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ConnectionConfig that = (ConnectionConfig) o;
return Objects.equals(connectionKey, that.connectionKey);
}
@Override
public int hashCode() {
return Objects.hash(connectionKey);
}
}
@@ -24,8 +24,10 @@ import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@Component
public class AgentEndpoint {
@@ -62,7 +64,7 @@ public class AgentEndpoint {
String netOID = jsonObject.getString("netOID");
if(StringUtils.isNotBlank(netOID)){
LinkedHashMap<String, String> map = JSON.parseObject(netOID, new TypeReference<LinkedHashMap<String, String>>() {});
GlobalConfig.SWITCH_NET_OID = map;
GlobalConfig.SWITCH_NET_OID.clear(); GlobalConfig.SWITCH_NET_OID.putAll(map);
LinkedHashMap<String,String> m = AgentUtil.swapMap(map);
if(m.containsKey(GlobalConfig.NET_INDEX_PARAM)){
GlobalConfig.NET_INDEX_OID = m.get(GlobalConfig.NET_INDEX_PARAM);
@@ -73,7 +75,7 @@ public class AgentEndpoint {
String moduleOID = jsonObject.getString("moduleOID");
if(StringUtils.isNotBlank(moduleOID)){
LinkedHashMap<String, String> map = JSON.parseObject(moduleOID, new TypeReference<LinkedHashMap<String, String>>() {});
GlobalConfig.SWITCH_MODULE_OID = map;
GlobalConfig.SWITCH_MODULE_OID.clear(); GlobalConfig.SWITCH_MODULE_OID.putAll(map);
LinkedHashMap<String,String> m = AgentUtil.swapMap(map);
if(m.containsKey(GlobalConfig.MODULE_INDEX_PARAM)){
GlobalConfig.MODULE_INDEX_OID = m.get(GlobalConfig.MODULE_INDEX_PARAM);
@@ -85,7 +87,7 @@ public class AgentEndpoint {
String mpuOID = jsonObject.getString("mpuOID");
if(StringUtils.isNotBlank(mpuOID)){
LinkedHashMap<String, String> map = JSON.parseObject(mpuOID, new TypeReference<LinkedHashMap<String, String>>() {});
GlobalConfig.SWITCH_MPU_OID = map;
GlobalConfig.SWITCH_MPU_OID.clear(); GlobalConfig.SWITCH_MPU_OID.putAll(map);
LinkedHashMap<String,String> m = AgentUtil.swapMap(map);
if(m.containsKey(GlobalConfig.MPU_INDEX_PARAM)){
GlobalConfig.MPU_INDEX_OID = m.get(GlobalConfig.MPU_INDEX_PARAM);
@@ -96,7 +98,7 @@ public class AgentEndpoint {
String pwrOID = jsonObject.getString("pwrOID");
if(StringUtils.isNotBlank(pwrOID)){
LinkedHashMap<String, String> map = JSON.parseObject(pwrOID, new TypeReference<LinkedHashMap<String, String>>() {});
GlobalConfig.SWITCH_PWR_OID = map;
GlobalConfig.SWITCH_PWR_OID.clear(); GlobalConfig.SWITCH_PWR_OID.putAll(map);
LinkedHashMap<String,String> m = AgentUtil.swapMap(map);
if(m.containsKey(GlobalConfig.PWR_INDEX_PARAM)){
GlobalConfig.PWR_INDEX_OID = m.get(GlobalConfig.PWR_INDEX_PARAM);
@@ -107,7 +109,7 @@ public class AgentEndpoint {
String fanOID = jsonObject.getString("fanOID");
if(StringUtils.isNotBlank(fanOID)){
LinkedHashMap<String, String> map = JSON.parseObject(fanOID, new TypeReference<LinkedHashMap<String, String>>() {});
GlobalConfig.SWITCH_FAN_OID = map;
GlobalConfig.SWITCH_FAN_OID.clear(); GlobalConfig.SWITCH_FAN_OID.putAll(map);
LinkedHashMap<String,String> m = AgentUtil.swapMap(map);
if(m.containsKey(GlobalConfig.FAN_INDEX_PARAM)){
GlobalConfig.FAN_INDEX_OID = m.get(GlobalConfig.FAN_INDEX_PARAM);
@@ -118,7 +120,7 @@ public class AgentEndpoint {
String otherOID = jsonObject.getString("otherOID");
if(StringUtils.isNotBlank(otherOID)){
LinkedHashMap<String, String> map = JSON.parseObject(otherOID, new TypeReference<LinkedHashMap<String, String>>() {});
GlobalConfig.SWITCH_OTHER_OID = map;
GlobalConfig.SWITCH_OTHER_OID.clear(); GlobalConfig.SWITCH_OTHER_OID.putAll(map);
LinkedHashMap<String,String> m = AgentUtil.swapMap(map);
if(m.containsKey(GlobalConfig.OTHER_INDEX_PARAM)){
GlobalConfig.OTHER_INDEX_OID = m.get(GlobalConfig.OTHER_INDEX_PARAM);
@@ -133,42 +135,42 @@ public class AgentEndpoint {
String netOID = object.getString("netOID");
if(StringUtils.isNotBlank(netOID)){
List<String> list = JSON.parseObject(netOID, new TypeReference<List<String>>() {});
GlobalConfig.NET_FILTER = list;
GlobalConfig.NET_FILTER.clear(); GlobalConfig.NET_FILTER.addAll(list);
}
}
if(object.containsKey("moduleOID")){
String moduleOID = object.getString("moduleOID");
if(StringUtils.isNotBlank(moduleOID)){
List<String> list = JSON.parseObject(moduleOID, new TypeReference<List<String>>() {});
GlobalConfig.MODULE_FILTER = list;
GlobalConfig.MODULE_FILTER.clear(); GlobalConfig.MODULE_FILTER.addAll(list);
}
}
if(object.containsKey("mpuOID")){
String mpuOID = object.getString("mpuOID");
if(StringUtils.isNotBlank(mpuOID)){
List<String> list = JSON.parseObject(mpuOID, new TypeReference<List<String>>() {});
GlobalConfig.MPU_FILTER = list;
GlobalConfig.MPU_FILTER.clear(); GlobalConfig.MPU_FILTER.addAll(list);
}
}
if(object.containsKey("pwrOID")){
String pwrOID = object.getString("pwrOID");
if(StringUtils.isNotBlank(pwrOID)){
List<String> list = JSON.parseObject(pwrOID, new TypeReference<List<String>>() {});
GlobalConfig.PWR_FILTER = list;
GlobalConfig.PWR_FILTER.clear(); GlobalConfig.PWR_FILTER.addAll(list);
}
}
if(object.containsKey("fanOID")){
String fanOID = object.getString("fanOID");
if(StringUtils.isNotBlank(fanOID)){
List<String> list = JSON.parseObject(fanOID, new TypeReference<List<String>>() {});
GlobalConfig.FAN_FILTER = list;
GlobalConfig.FAN_FILTER.clear(); GlobalConfig.FAN_FILTER.addAll(list);
}
}
if(object.containsKey("otherOID")){
String otherOID = object.getString("otherOID");
if(StringUtils.isNotBlank(otherOID)){
List<String> list = JSON.parseObject(otherOID, new TypeReference<List<String>>() {});
GlobalConfig.OTHER_FILTER = list;
GlobalConfig.OTHER_FILTER.clear(); GlobalConfig.OTHER_FILTER.addAll(list);
}
}
}
@@ -293,7 +295,8 @@ public class AgentEndpoint {
String alarms = jsonObject.getString("alarms");
if(StringUtils.isNotBlank(alarms)){
AssertLog.info("告警设置,alarms={}", alarms);
GlobalConfig.ALARM_LIST = JSON.parseObject(alarms, new TypeReference<List<AlarmEO>>() {});
List<AlarmEO> alarmList = JSON.parseObject(alarms, new TypeReference<List<AlarmEO>>() {});
GlobalConfig.ALARM_LIST.clear(); GlobalConfig.ALARM_LIST.addAll(alarmList);
AssertLog.info("告警设置,监控项={}", JSON.toJSONString(GlobalConfig.ALARM_LIST));
if(CollectionUtil.isNotEmpty(GlobalConfig.ALARM_LIST)){
AssertLog.info("告警设置,is_alarm={}", AgentDataUtil.hasAnyActiveAlarm(GlobalConfig.ALARM_LIST));
@@ -347,74 +350,60 @@ public class AgentEndpoint {
if(AdvancedAsyncDownloader.createSingleDirectoryIfNotExists(policy.getFilePath())){
if(CollectionUtil.isNotEmpty(policy.getFiles())){
isFalse = true;
GlobalConfig.DOWN_FILES.put(policy.getPolicyName(), policy.getFiles().size());
// 修复:使用 AtomicInteger 替代 Integer,线程安全
GlobalConfig.DOWN_FILES.put(policy.getPolicyName(), new AtomicInteger(policy.getFiles().size()));
for (ScriptPolicyEO.ScriptFile f : policy.getFiles()) {
String finalSaveDir = policy.getFilePath();
//文件类型:0、平台文件地址;1、外网HTTP(S)
if(f.getFileType() == 0){
String filePath = policy.getFilePath() + "/" + f.getFileName();
// 移除Base64 URL前缀(如果存在)
// if (f.getFileData().contains(",")) {
// f.setFileData(f.getFileData().split(",")[1]);
// }
// 修复:路径穿越防护
String fileName = f.getFileName();
java.nio.file.Path baseDir = Paths.get(finalSaveDir).toAbsolutePath().normalize();
java.nio.file.Path targetPath = baseDir.resolve(fileName).normalize();
if (!targetPath.startsWith(baseDir)) {
AssertLog.error("检测到路径穿越攻击,拒绝写入文件: {}", fileName);
continue;
}
String filePath = targetPath.toString();
try {
AgentUtil.base64ToFile(f.getFileData(), filePath);
System.out.println("文件保存成功: " + filePath);
AssertLog.info("文件保存成功: {}", filePath);
isFalse = true;
try {
AgentDataUtil.chmod(filePath,"+x");
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("设置文件权限失败", e);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
AssertLog.error("设置文件权限被中断", e);
}
} catch (IOException e) {
isFalse = false;
System.err.println("文件保存失败: " + e.getMessage());
AssertLog.error("文件保存失败: {}", e.getMessage());
}
}else{
AdvancedAsyncDownloader.downloadWithProgress(f.getFileUrl(), finalSaveDir, f.getFileName(), progress -> {
System.out.printf("下载进度: %.1f%%\n", progress);
AssertLog.info("下载进度: {}%", String.format("%.1f", progress));
}).thenAccept(filePath -> {
System.out.println("下载完成: " + filePath);
AssertLog.info("下载完成: {}", filePath);
try {
AgentDataUtil.chmod(finalSaveDir+"/"+f.getFileName(),"775");
int count = GlobalConfig.DOWN_FILES.get(policy.getPolicyName());
if(count > 1){
GlobalConfig.DOWN_FILES.put(policy.getPolicyName(), count--);
}else{
// 修复:使用 AtomicInteger decrementAndGet 线程安全递减
AtomicInteger counter = GlobalConfig.DOWN_FILES.get(policy.getPolicyName());
if (counter != null && counter.decrementAndGet() <= 0) {
//所有文件下载完成,执行脚本命令
agentService.command(policy, clientId,MsgEnum.执行脚本策略应答.getValue());
}
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("设置文件权限失败", e);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
AssertLog.error("设置文件权限被中断", e);
}
}).exceptionally(ex -> {
System.err.println("下载错误: " + ex.getMessage());
AssertLog.error("下载错误: {}", ex.getMessage());
return null;
});
// Java8FileDownloader.DownloadResult result = Java8FileDownloader.downloadFile(f.getFileUrl(), finalSaveDir, f.getFileName());
// // 验证下载是否完成
// if (result.isSuccess()) {
// boolean isComplete = Java8FileDownloader.verifyFileCompletion(
// result.getFilePath(), result.getExpectedSize());
// if(isComplete){
// isFalse = true;
// System.out.println("下载完成: " + finalSaveDir+"/"+f.getFileName());
// try {
// AgentDataUtil.chmod(finalSaveDir+"/"+f.getFileName(),"775");
// } catch (IOException e) {
// e.printStackTrace();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }else{
// isFalse = false;
// System.err.println("下载失败");
// }
// }
}
}
}
@@ -453,9 +442,9 @@ public class AgentEndpoint {
isFalse = true;
String finalSaveDir = versionUpdateEO.getFilePath();
AdvancedAsyncDownloader.downloadWithProgress(versionUpdateEO.getFileUrl(), finalSaveDir, null, progress -> {
System.out.printf("下载进度: %.1f%%\n", progress);
AssertLog.info("版本更新下载进度: {}%", String.format("%.1f", progress));
}).thenAccept(filePath -> {
System.out.println("下载完成: " + filePath);
AssertLog.info("版本更新下载完成: {}", filePath);
try {
//更改全局变量
GlobalConfig.isCollect = false;
@@ -464,10 +453,10 @@ public class AgentEndpoint {
//所有文件下载完成,执行脚本命令
agentService.command(policy, clientId,MsgEnum.Agent版本更新应答.getValue());
} catch (Exception e) {
e.printStackTrace();
AssertLog.error("版本更新执行异常", e);
}
}).exceptionally(ex -> {
System.err.println("下载错误: " + ex.getMessage());
AssertLog.error("版本更新下载错误: {}", ex.getMessage());
return null;
});
}
@@ -1,5 +1,6 @@
package com.tongran.agent.client.netty.handler;
import com.tongran.agent.client.utils.AssertLog;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
@@ -16,9 +17,24 @@ public class UDPListenHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (!(msg instanceof DatagramPacket)) {
return;
}
DatagramPacket packet = (DatagramPacket) msg;
ByteBuf buf = packet.content();
buf.clear();// 暂未实现
try {
ByteBuf buf = packet.content();
byte[] data = new byte[buf.readableBytes()];
buf.readBytes(data);
AssertLog.info("收到UDP数据: {} bytes, 来自: {}", data.length, packet.sender());
// TODO: 实现UDP消息处理逻辑
} finally {
// 修复:确保 DatagramPacket 被释放,避免内存泄漏
packet.release();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
AssertLog.error("UDP处理异常", cause);
}
}
@@ -1,5 +1,7 @@
package com.tongran.agent.client.scheduler.service;
import com.tongran.agent.client.utils.AssertLog;
import java.io.BufferedInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -8,114 +10,162 @@ import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
* 异步文件下载器
* 安全增强:协议白名单、域名白名单、文件大小上限、路径穿越防护
*/
public class AdvancedAsyncDownloader {
private static final ExecutorService downloadExecutor =
Executors.newFixedThreadPool(5);
private static final ExecutorService downloadExecutor =
Executors.newFixedThreadPool(5, r -> {
Thread t = new Thread(r, "async-downloader");
t.setDaemon(true);
return t;
});
/** 允许的下载协议 */
private static final Set<String> ALLOWED_PROTOCOLS = new HashSet<>();
static {
ALLOWED_PROTOCOLS.add("http");
ALLOWED_PROTOCOLS.add("https");
}
/** 最大下载文件大小 (200MB) */
private static final long MAX_FILE_SIZE = 200L * 1024 * 1024;
/** 连接超时 (10秒) */
private static final int CONNECT_TIMEOUT = 10_000;
/** 读取超时 (60秒) */
private static final int READ_TIMEOUT = 60_000;
/**
* 带进度回调的异步下载
* 带进度回调的异步下载(安全增强版)
*/
public static CompletableFuture<String> downloadWithProgress(
String fileUrl,
String saveDir,
String fileUrl,
String saveDir,
String fileName,
Consumer<Double> progressCallback) {
return CompletableFuture.supplyAsync(() -> {
HttpURLConnection connection = null;
try {
// 安全校验:协议白名单
URL url = new URL(fileUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 获取文件大小
if (!ALLOWED_PROTOCOLS.contains(url.getProtocol().toLowerCase())) {
throw new SecurityException("不允许的协议: " + url.getProtocol());
}
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(CONNECT_TIMEOUT);
connection.setReadTimeout(READ_TIMEOUT);
// 获取文件大小并校验
long fileSize = connection.getContentLengthLong();
if (fileSize > MAX_FILE_SIZE) {
throw new SecurityException("文件大小超过限制: " + fileSize + " bytes");
}
// 创建保存目录
Path directory = Paths.get(saveDir);
if (!Files.exists(directory)) {
Files.createDirectories(directory);
}
String filePath = directory.resolve(fileName != null ? fileName :
extractFileNameFromUrl(fileUrl)).toString();
// 确定文件名
String resolvedFileName = fileName != null ? fileName : extractFileNameFromUrl(fileUrl);
Path filePath = directory.resolve(resolvedFileName).normalize();
// 路径穿越防护:确保最终路径在允许的目录内
if (!filePath.startsWith(directory)) {
throw new SecurityException("检测到路径穿越攻击: " + resolvedFileName);
}
try (BufferedInputStream in = new BufferedInputStream(connection.getInputStream());
FileOutputStream out = new FileOutputStream(filePath)) {
FileOutputStream out = new FileOutputStream(filePath.toFile())) {
byte[] buffer = new byte[8192];
int bytesRead;
long totalRead = 0;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
totalRead += bytesRead;
// 回调进度
// 文件大小运行时校验
if (totalRead > MAX_FILE_SIZE) {
throw new SecurityException("下载文件大小超过运行时限制");
}
if (progressCallback != null && fileSize > 0) {
double progress = (double) totalRead / fileSize * 100;
progressCallback.accept(progress);
}
}
}
return filePath;
return filePath.toString();
} catch (Exception e) {
throw new RuntimeException("下载失败", e);
AssertLog.error("文件下载失败: {} - {}", fileUrl, e.getMessage());
throw new RuntimeException("下载失败: " + e.getMessage(), e);
} finally {
// 修复:确保 connection 被释放
if (connection != null) {
connection.disconnect();
}
}
}, downloadExecutor);
}
public static String extractFileNameFromUrl(String fileUrl) {
// 实现文件名提取逻辑
return fileUrl.substring(fileUrl.lastIndexOf('/') + 1);
String result = fileUrl.substring(fileUrl.lastIndexOf('/') + 1);
// 去除查询参数
int queryIndex = result.indexOf('?');
if (queryIndex > 0) {
result = result.substring(0, queryIndex);
}
return result.isEmpty() ? "download_file" : result;
}
/**
* 检查目录是否存在,不存在则创建(不创建父目录)
* @param directoryPath 目录路径
* @return 创建成功返回true,否则返回false
*/
public static boolean createSingleDirectoryIfNotExists(String directoryPath) {
try {
Path path = Paths.get(directoryPath);
if (!Files.exists(path)) {
// 只创建单级目录(父目录必须存在)
Files.createDirectory(path);
System.out.println("目录创建成功: " + directoryPath);
return true;
} else {
System.out.println("目录已存在: " + directoryPath);
AssertLog.info("目录创建成功: {}", directoryPath);
return true;
}
return true;
} catch (IOException e) {
System.err.println("创建目录失败: " + directoryPath);
System.err.println("错误信息: " + e.getMessage());
AssertLog.error("创建目录失败: {} - {}", directoryPath, e.getMessage());
return false;
}
}
/**
* 使用示例
* 优雅关闭线程池
*/
public static void main(String[] args) {
String fileUrl = "https://www.tzdsp.net/ksc-andromedae";
String saveDir = "D:/down";
downloadWithProgress(fileUrl, saveDir, null, progress -> {
System.out.printf("下载进度: %.1f%%\n", progress);
}).thenAccept(filePath -> {
System.out.println("下载完成: " + filePath);
}).exceptionally(ex -> {
System.err.println("下载错误: " + ex.getMessage());
return null;
});
// 主线程继续执行
System.out.println("异步下载已启动...");
public static void shutdown() {
downloadExecutor.shutdown();
try {
if (!downloadExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
downloadExecutor.shutdownNow();
}
} catch (InterruptedException e) {
downloadExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
}
@@ -1,18 +1,33 @@
package com.tongran.agent.client.scheduler.service;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.tongran.agent.client.core.config.GlobalConfig;
import com.tongran.agent.client.core.enums.MsgEnum;
import com.tongran.agent.client.netty.MultiTargetNettyClient;
import com.tongran.agent.client.netty.model.Message;
import com.tongran.agent.client.service.AgentService;
import com.tongran.agent.client.utils.AgentUtil;
import com.tongran.agent.client.utils.AssertLog;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Component
public class AppInitializer implements CommandLineRunner {
private final BusinessTasks businessTasks;
private final DynamicTaskService dynamicTaskService;
// 使用构造函数注入,避免循环依赖
@Resource
private MultiTargetNettyClient client;
@Resource
private AgentService agentService;
public AppInitializer(DynamicTaskService dynamicTaskService,
@Lazy BusinessTasks businessTasks) {
this.dynamicTaskService = dynamicTaskService;
@@ -21,64 +36,90 @@ public class AppInitializer implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("应用启动完成,开始初始化定时任务...");
if(GlobalConfig.isCollect){
// 开启定时任务
initScheduledTasks();
AssertLog.info("应用启动完成,开始初始化...");
// 创建SaaS连接
initConnection();
AssertLog.info("初始化完成");
}
/**
* 初始化SaaS连接并启动定时任务
*/
private void initConnection() {
boolean success = true;
int activeConnect = client.getActiveConnections();
AssertLog.info("当前活跃连接数: {}", activeConnect);
if (activeConnect == 0) {
success = agentService.connection();
}
if (success) {
AssertLog.info("连接成功,发送初始连接消息");
if (GlobalConfig.isRegister) {
// 已注册,启动各项定时任务
agentService.addRoute(null, null);
AssertLog.info("启动心跳定时任务 - 延迟: {}ms, 间隔: {}ms", 15000, 30000);
dynamicTaskService.scheduleTask("heartbeat", businessTasks::heartbeatTask, 15000L, 30000L);
long milli = AgentUtil.getMillisToNextMinute() + 60000L;
AssertLog.info("启动更新策略定时任务 - 延迟: {}ms, 间隔: {}ms", milli, 60000);
dynamicTaskService.scheduleTask("policy", businessTasks::policyTask, milli, 60000L);
AssertLog.info("启动多网IP探测定时任务 - 延迟: {}ms, 间隔: {}ms", milli, 300000);
dynamicTaskService.scheduleTask("networkDetect", businessTasks::networkDetectTask, milli, 300000L);
AssertLog.info("检测监控策略配置");
agentService.checkMonitor();
AssertLog.info("检测agent更新配置");
agentService.checkAgentUpdate();
AssertLog.info("启动frpc保活定时任务 - 延迟: {}ms, 间隔: {}ms", 15000, 600000);
dynamicTaskService.scheduleTask("checkFrpc", businessTasks::checkFrpcTask, 15000L, 600000L);
AssertLog.info("启动frpc状态上报定时任务 - 延迟: {}ms, 间隔: {}ms", 15000, 300000);
dynamicTaskService.scheduleTask("upFrpcMsg", businessTasks::upFrpcMsgTask, 15000L, 300000L);
long milliFive = AgentUtil.millisecondsToNext5Minute();
AssertLog.info("启动本地存储流量信息上报定时任务 - 延迟: {}ms, 间隔: {}ms", milliFive, 0x6DDD00L);
dynamicTaskService.scheduleTask("upTempTraffic", businessTasks::upTempTraffic, milliFive, 0x6DDD00L);
AssertLog.info("检测PppoE配置");
agentService.handleRebootRecovery();
AssertLog.info("检测tcpdump探测时间配置");
agentService.checkTcpdumpTimes();
} else {
// 未注册,发送注册消息
try {
if (StringUtils.isBlank(GlobalConfig.DEVICE_SN)) {
GlobalConfig.DEVICE_SN = AgentUtil.getDeviceSN();
}
long timestamps = System.currentTimeMillis();
timestamps = Math.round(timestamps / 1000.0);
JSONObject objects = new JSONObject();
objects.put("clientId", GlobalConfig.CLIENT_ID);
objects.put("sn", GlobalConfig.DEVICE_SN);
objects.put("timestamp", timestamps);
Message message = Message.builder()
.clientId(GlobalConfig.CLIENT_ID)
.dataType(MsgEnum.注册.getValue())
.data(objects.toString())
.build();
client.sendMessages(GlobalConfig.CLIENT_ID, message);
} catch (Exception e) {
AssertLog.error("发送注册消息失败", e);
}
// 定时重试注册
dynamicTaskService.scheduleTask("register", businessTasks::registerTask, 60000L, 300000L);
}
} else {
// 连接失败,定时重试
AssertLog.error("SaaS连接失败,启动重连定时任务");
dynamicTaskService.scheduleTask("connection", businessTasks::connectionTask, 60000L, 180000L);
}
System.out.println("定时任务初始化完成");
}
private void initScheduledTasks() {
// 每30秒执行心跳任务
AssertLog.info("初始化启动心跳定时任务 - 延迟: {}ms, 间隔: {}ms", 15000, 30000);
dynamicTaskService.scheduleTask("heartbeat",
businessTasks::heartbeatTask, 15000, 30000);
// long milli = AgentUtil.getMillisToNextMinute() + 60000;
// // 每300秒执行CPU信息采集任务
// AssertLog.info("初始化启动CPU信息采集定时任务 - 延迟: {}ms, 间隔: {}ms", milli, 300000);
// dynamicTaskService.scheduleTask("cpu",
// businessTasks::cpuTask, milli, 300000); // 25秒后开始,每300秒执行
//
// // 每300秒执行磁盘信息采集任务
// AssertLog.info("初始化启动磁盘信息采集定时任务 - 延迟: {}ms, 间隔: {}ms", milli, 300000);
// dynamicTaskService.scheduleTask("disk",
// businessTasks::diskTask, milli, 300000); // 35秒后开始,每300秒执行
//
// // 每300秒执行系统信息采集任务
// AssertLog.info("初始化启动系统信息采集定时任务 - 延迟: {}ms, 间隔: {}ms", milli, 300000);
// dynamicTaskService.scheduleTask("system",
// businessTasks::systemTask, milli, 300000); // 40秒后开始,每300秒执行
//
// // 每300秒执行容器信息采集任务
// AssertLog.info("初始化启动容器信息采集定时任务 - 延迟: {}ms, 间隔: {}ms", milli, 300000);
// dynamicTaskService.scheduleTask("docker",
// businessTasks::dockerTask, milli, 300000); // 45秒后开始,每300秒执行
//
// // 每300秒执行内存信息采集任务
// AssertLog.info("初始化启动内存信息采集定时任务 - 延迟: {}ms, 间隔: {}ms", milli, 300000);
// dynamicTaskService.scheduleTask("memory",
// businessTasks::memoryTask, milli, 300000); // 55秒后开始,每300秒执行
//
//
// //获取当前时间距离下一个“分钟为 0 或 5”的时间点还差多少毫秒
// long millis = AgentUtil.millisecondsToNext5Minute();
// // 每300秒执行网络信息采集任务
// AssertLog.info("初始化启动网络信息采集定时任务 - 延迟: {}ms, 间隔: {}ms", millis, 300000);
// dynamicTaskService.scheduleTask("net",
// businessTasks::netTask, millis, 300000); // 65秒后开始,每300秒执行
//
// // 每300秒执行挂载信息采集任务
// AssertLog.info("初始化启动挂载信息采集定时任务 - 延迟: {}ms, 间隔: {}ms", milli, 300000);
// dynamicTaskService.scheduleTask("point",
// businessTasks::pointTask, milli, 300000); // 75秒后开始,每300秒执行
//
// // 每300秒执行交换机信息采集任务
// AssertLog.info("初始化启动交换机信息采集定时任务 - 延迟: {}ms, 间隔: {}ms", millis, 300000);
// dynamicTaskService.scheduleTask("switchBoard",
// businessTasks::switchBoardTask, millis, 300000); // 85秒后开始,每300秒执行
}
}
}
@@ -1,5 +1,7 @@
package com.tongran.agent.client.scheduler.service;
import com.tongran.agent.client.utils.AssertLog;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
@@ -13,12 +15,24 @@ import java.util.Set;
import java.util.concurrent.*;
import java.util.function.Consumer;
/**
* 异步命令执行器
* 修复:使用有界线程池替代无界 CachedThreadPool,使用守护线程
*/
public class AsyncCommandExecutor {
// 使用线程池管理异步任务
private static final ExecutorService executorService = Executors.newCachedThreadPool();
private static final ScheduledExecutorService timeoutExecutor = Executors.newScheduledThreadPool(5);
// 修复:使用有界线程池替代无界 CachedThreadPool
private static final ExecutorService executorService = Executors.newFixedThreadPool(20, r -> {
Thread t = new Thread(r, "async-cmd-executor");
t.setDaemon(true);
return t;
});
private static final ScheduledExecutorService timeoutExecutor = Executors.newScheduledThreadPool(5, r -> {
Thread t = new Thread(r, "async-timeout-monitor");
t.setDaemon(true);
return t;
});
/**
* 异步为文件添加可执行权限
*/
@@ -26,25 +40,26 @@ public class AsyncCommandExecutor {
return CompletableFuture.supplyAsync(() -> {
try {
Path path = Paths.get(filePath);
if (!Files.exists(path)) {
throw new RuntimeException("文件不存在: " + filePath);
}
Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(path);
permissions.add(PosixFilePermission.OWNER_EXECUTE);
permissions.add(PosixFilePermission.GROUP_EXECUTE);
permissions.add(PosixFilePermission.OTHERS_EXECUTE);
Files.setPosixFilePermissions(path, permissions);
return true;
} catch (Exception e) {
AssertLog.error("设置执行权限失败: {}", e.getMessage());
throw new RuntimeException("设置执行权限失败: " + e.getMessage(), e);
}
}, executorService);
}
/**
* 异步执行命令(基础版本)
*/
@@ -52,55 +67,54 @@ public class AsyncCommandExecutor {
List<String> command,
long timeout,
TimeUnit timeUnit) {
return CompletableFuture.supplyAsync(() -> {
Process process = null;
try {
ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.redirectErrorStream(true);
// processBuilder.directory(new File("/data/agent-server")); // 设置工作目录
// 不合并错误流,分别读取
process = processBuilder.start();
// 异步读取输出
Process finalProcess = process;
Future<String> outputFuture = executorService.submit(() -> {
StringBuilder output = new StringBuilder();
// String output = "";
try (BufferedReader reader = new BufferedReader(new InputStreamReader(finalProcess.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
// output = line;
}
} catch (IOException e) {
AssertLog.error("读取命令输出异常", e);
}
return output.toString();
});
// 设置超时
if (timeout > 0) {
boolean finished = process.waitFor(timeout, timeUnit);
if (!finished) {
process.destroy();
process.destroyForcibly();
throw new TimeoutException("命令执行超时");
}
} else {
process.waitFor();
}
String output = outputFuture.get(1, TimeUnit.SECONDS);
int exitCode = process.exitValue();
return new CommandResult(exitCode, output, "");
} catch (Exception e) {
AssertLog.error("命令执行失败: {}", e.getMessage());
return new CommandResult(-1, "", "执行失败: " + e.getMessage());
} finally {
if (process != null) {
process.destroy();
process.destroyForcibly();
}
}
}, executorService);
}
/**
* 带实时输出的异步命令执行
*/
@@ -110,66 +124,67 @@ public class AsyncCommandExecutor {
TimeUnit timeUnit,
Consumer<String> outputConsumer,
Consumer<String> errorConsumer) {
return CompletableFuture.supplyAsync(() -> {
Process process = null;
try {
ProcessBuilder processBuilder = new ProcessBuilder(command);
process = processBuilder.start();
// 启动输出读取线程
CompletableFuture<Void> outputFuture = readStreamAsync(
process.getInputStream(), outputConsumer, "OUTPUT");
process.getInputStream(), outputConsumer, "OUTPUT");
CompletableFuture<Void> errorFuture = readStreamAsync(
process.getErrorStream(), errorConsumer, "ERROR");
process.getErrorStream(), errorConsumer, "ERROR");
// 设置超时监控
ScheduledFuture<?> timeoutFuture = null;
if (timeout > 0) {
Process finalProcess = process;
timeoutFuture = timeoutExecutor.schedule(() -> {
if (finalProcess.isAlive()) {
finalProcess.destroy();
finalProcess.destroyForcibly();
}
}, timeout, timeUnit);
}
// 等待进程完成
int exitCode = process.waitFor();
// 取消超时任务
if (timeoutFuture != null) {
timeoutFuture.cancel(false);
}
// 等待输出读取完成
CompletableFuture.allOf(outputFuture, errorFuture).get(2, TimeUnit.SECONDS);
return new CommandResult(exitCode, "", "");
} catch (Exception e) {
AssertLog.error("实时输出命令执行异常: {}", e.getMessage());
return new CommandResult(-1, "", "执行异常: " + e.getMessage());
} finally {
if (process != null) {
process.destroy();
process.destroyForcibly();
}
}
}, executorService);
}
/**
* 异步读取流数据
*/
private static CompletableFuture<Void> readStreamAsync(
InputStream inputStream,
InputStream inputStream,
Consumer<String> lineConsumer,
String streamType) {
return CompletableFuture.runAsync(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(inputStream))) {
new InputStreamReader(inputStream))) {
String line;
while ((line = reader.readLine()) != null) {
if (lineConsumer != null) {
@@ -183,7 +198,7 @@ public class AsyncCommandExecutor {
}
}, executorService);
}
/**
* 异步执行脚本文件
*/
@@ -193,38 +208,25 @@ public class AsyncCommandExecutor {
long timeout,
TimeUnit timeUnit,
Consumer<String> realtimeOutputConsumer) {
return makeFileExecutableAsync(scriptPath)
.thenCompose(permissionSuccess -> {
if (!permissionSuccess) {
return CompletableFuture.completedFuture(
new CommandResult(-1, "", "设置执行权限失败"));
}
String[] command = new String[args.length + 1];
command[0] = scriptPath;
System.arraycopy(args, 0, command, 1, args.length);
return executeCommandWithRealtimeOutput(
command, timeout, timeUnit,
realtimeOutputConsumer,
error -> System.err.println(error));
});
.thenCompose(permissionSuccess -> {
if (!permissionSuccess) {
return CompletableFuture.completedFuture(
new CommandResult(-1, "", "设置执行权限失败"));
}
String[] command = new String[args.length + 1];
command[0] = scriptPath;
System.arraycopy(args, 0, command, 1, args.length);
return executeCommandWithRealtimeOutput(
command, timeout, timeUnit,
realtimeOutputConsumer,
error -> AssertLog.error("脚本执行错误输出: {}", error));
});
}
/**
* 批量异步执行命令
*/
// public static List<CompletableFuture<CommandResult>> executeCommandsAsync(
// List<String[]> commands,
// long timeout,
// TimeUnit timeUnit) {
//
// return commands.stream()
// .map(command -> executeCommandAsync(command, timeout, timeUnit))
// .collect(java.util.stream.Collectors.toList());
// }
/**
* 关闭执行器
*/
@@ -244,7 +246,7 @@ public class AsyncCommandExecutor {
Thread.currentThread().interrupt();
}
}
/**
* 执行结果封装类
*/
@@ -252,22 +254,22 @@ public class AsyncCommandExecutor {
private final int exitCode;
private final String output;
private final String error;
public CommandResult(int exitCode, String output, String error) {
this.exitCode = exitCode;
this.output = output;
this.error = error;
}
public int getExitCode() { return exitCode; }
public String getOutput() { return output; }
public String getError() { return error; }
public boolean isSuccess() { return exitCode == 0; }
@Override
public String toString() {
return String.format("Exit Code: %d\nOutput: %s\nError: %s",
exitCode, output, error);
return String.format("Exit Code: %d\nOutput: %s\nError: %s",
exitCode, output, error);
}
}
}
}
@@ -9,6 +9,8 @@ import com.tongran.agent.client.core.config.GlobalConfig;
import com.tongran.agent.client.core.enums.MsgEnum;
import com.tongran.agent.client.core.session.SessionManager;
import com.tongran.agent.client.core.vo.*;
import com.tongran.agent.client.netty.MultiTargetNettyClient;
import com.tongran.agent.client.netty.config.AgentNettyConfig;
import com.tongran.agent.client.netty.model.Message;
import com.tongran.agent.client.service.*;
import com.tongran.agent.client.utils.AgentUtil;
@@ -116,6 +118,18 @@ public class BusinessTasks {
@Resource
private ApplicationProperties properties;
@Resource
private MultiTargetNettyClient client;
@Resource
private AgentNettyConfig config;
@Resource
private AgentService agentService;
@Resource
private NetBusinessService netBusinessService;
/**
* 任务1:心跳上报任务
*/
@@ -125,19 +139,34 @@ public class BusinessTasks {
timestamp = Math.round(timestamp / 1000.0);
int count = heartbeatTask.incrementAndGet();
AssertLog.info("心跳定时任务执行 - 时间: {},task #{}", LocalDateTime.now(), count);
// 业务处理
// 判定客户端与服务端是否连接
if (Objects.nonNull(sessionManager.getSessionById(GlobalConfig.CLIENT_ID))) {
// 发送心跳包
JSONObject object = new JSONObject();
object.put("strength","31");
object.put("name", properties.getName());
object.put("version", properties.getVersion());
object.put("startupTime", GlobalConfig.startupTime);
object.put("timestamp",timestamp);
Message message = Message.builder().clientId(GlobalConfig.CLIENT_ID).dataType(MsgEnum.心跳上报.getValue()).data(object.toString()).build();
sessionManager.writeAndFlush(sessionManager.getSessionById(GlobalConfig.CLIENT_ID).getChannel(), message);
AssertLog.info("发送心跳包={}",JSON.toJSONString(message));
// 检查活跃连接,如果断开则尝试重连
boolean success = true;
int activeConnect = client.getActiveConnections();
if (activeConnect == 0) {
success = agentService.connection();
}
if (success) {
// 判定客户端与服务端是否连接
if (Objects.nonNull(sessionManager.getSessionById(GlobalConfig.CLIENT_ID))) {
if (StringUtils.isBlank(GlobalConfig.DEVICE_SN)) {
GlobalConfig.DEVICE_SN = AgentUtil.getDeviceSN();
}
// 发送心跳包
JSONObject object = new JSONObject();
object.put("clientId", GlobalConfig.CLIENT_ID);
object.put("logicalNode", agentService.getLogicalNode());
object.put("sn", GlobalConfig.DEVICE_SN);
object.put("strength","31");
object.put("name", properties.getName());
object.put("version", properties.getVersion());
object.put("startupTime", GlobalConfig.startupTime);
object.put("timestamp",timestamp);
Message message = Message.builder().clientId(GlobalConfig.CLIENT_ID).dataType(MsgEnum.心跳上报.getValue()).data(object.toString()).build();
sessionManager.writeAndFlush(sessionManager.getSessionById(GlobalConfig.CLIENT_ID).getChannel(), message);
AssertLog.info("发送心跳包={}",JSON.toJSONString(message));
}
} else {
AssertLog.info("心跳定时任务执行失败-连接断开");
}
AssertLog.info("心跳定时任务执行 - task #{} completed", count);
}
@@ -1270,4 +1299,221 @@ public class BusinessTasks {
// ==================== 定时任务方法(从1.20恢复) ====================
/**
* 策略更新定时任务
*/
@Async("taskExecutor")
public void policyTask() {
long timestamp = AgentUtil.roundMinutes();
int count = new AtomicInteger(0).incrementAndGet();
AssertLog.info("获取最新策略定时任务执行 - 时间: {},task #{}", LocalDateTime.now(), count);
if (Objects.nonNull(sessionManager.getSessionById(GlobalConfig.CLIENT_ID))) {
if (StringUtils.isBlank(GlobalConfig.DEVICE_SN)) {
GlobalConfig.DEVICE_SN = AgentUtil.getDeviceSN();
}
JSONObject object = new JSONObject();
object.put("clientId", GlobalConfig.CLIENT_ID);
object.put("sn", GlobalConfig.DEVICE_SN);
object.put("timestamp", timestamp);
Message message = Message.builder().clientId(GlobalConfig.CLIENT_ID).dataType(MsgEnum.获取最新策略.getValue()).data(object.toString()).build();
sessionManager.writeAndFlush(sessionManager.getSessionById(GlobalConfig.CLIENT_ID).getChannel(), message);
AssertLog.info("发送获取最新策略信息包={}", JSON.toJSONString(message));
}
AssertLog.info("获取最新策略定时任务执行 - task #{} completed", count);
}
/**
* 多网IP探测定时任务
*/
@Async("taskExecutor")
public void networkDetectTask() {
int count = new AtomicInteger(0).incrementAndGet();
AssertLog.info("多网IP探测定时任务执行 - 时间: {}task #{}", LocalDateTime.now(), count);
if (Objects.nonNull(sessionManager.getSessionById(GlobalConfig.CLIENT_ID))) {
if (StringUtils.isBlank(GlobalConfig.DEVICE_SN)) {
GlobalConfig.DEVICE_SN = AgentUtil.getDeviceSN();
}
long timestamps = System.currentTimeMillis();
timestamps = Math.round(timestamps / 1000.0);
List infos = null;
try {
// NetworkInterfaceUtil may not be available in all environments
infos = com.tongran.agent.client.utils.NetworkInterfaceUtil.collectNetworkInfo();
} catch (Exception e) {
AssertLog.warn("collectNetworkInfo failed: {}", e.getMessage());
}
JSONObject objects = new JSONObject();
objects.put("clientId", GlobalConfig.CLIENT_ID);
objects.put("sn", GlobalConfig.DEVICE_SN);
objects.put("networkInfo", JSON.toJSONString(infos));
objects.put("timestamp", timestamps);
Message message = Message.builder().clientId(GlobalConfig.CLIENT_ID).dataType(MsgEnum.多网IP探测上报.getValue()).data(objects.toString()).build();
sessionManager.writeAndFlush(sessionManager.getSessionById(GlobalConfig.CLIENT_ID).getChannel(), message);
AssertLog.info("发送多网IP探测信息包={}", JSON.toJSONString(message));
}
AssertLog.info("发送多网IP探测定时任务执行 - task #{} completed", count);
}
/**
* FRPC保活定时任务
*/
@Async("taskExecutor")
public void checkFrpcTask() {
int count = new AtomicInteger(0).incrementAndGet();
AssertLog.info("frpc保活机制定时任务执行 - 时间: {},task #{}", LocalDateTime.now(), count);
if (Objects.nonNull(sessionManager.getSessionById(GlobalConfig.CLIENT_ID))) {
agentService.checkFrpc();
} else {
AssertLog.info("断开连接--frpc保活机制定时任务执行 - task #{} completed", count);
}
}
/**
* FRPC状态上报定时任务
*/
@Async("taskExecutor")
public void upFrpcMsgTask() {
int count = new AtomicInteger(0).incrementAndGet();
AssertLog.info("frpc状态上报定时任务执行 - 时间: {},task #{}", LocalDateTime.now(), count);
if (Objects.nonNull(sessionManager.getSessionById(GlobalConfig.CLIENT_ID))) {
agentService.upFrpcMsg();
} else {
AssertLog.info("断开连接--frpc状态上报定时任务执行 - task #{} completed", count);
}
}
/**
* 本地存储流量信息上报定时任务
*/
@Async("taskExecutor")
public void upTempTraffic() {
int count = new AtomicInteger(0).incrementAndGet();
AssertLog.info("本地存储流量信息上报定时任务执行 - 时间: {},task #{}", LocalDateTime.now(), count);
if (Objects.nonNull(sessionManager.getSessionById(GlobalConfig.CLIENT_ID))) {
agentService.checkTraffic();
}
AssertLog.info("本地存储流量信息上报定时任务执行 - task #{} completed", count);
}
/**
* 注册重试定时任务
*/
@Async("taskExecutor")
public void registerTask() {
long timestamp = System.currentTimeMillis();
timestamp = Math.round(timestamp / 1000.0);
int count = new AtomicInteger(0).incrementAndGet();
AssertLog.info("注册重试定时任务执行 - 时间: {},task #{}", LocalDateTime.now(), count);
boolean success = true;
int activeConnect = client.getActiveConnections();
AssertLog.info("注册重试定时任务执行 - 时间: {}activeConnect={}", LocalDateTime.now(), activeConnect);
if (activeConnect == 0 || count > 1) {
success = agentService.connection();
}
if (success) {
AssertLog.info("连接成功,发送初始连接消息");
try {
if (Objects.nonNull(sessionManager.getSessionById(GlobalConfig.CLIENT_ID))) {
if (StringUtils.isBlank(GlobalConfig.DEVICE_SN)) {
GlobalConfig.DEVICE_SN = AgentUtil.getDeviceSN();
}
List infos = null;
try {
infos = com.tongran.agent.client.utils.NetworkInterfaceUtil.collectNetworkInfo();
} catch (Exception e) {
AssertLog.warn("collectNetworkInfo failed: {}", e.getMessage());
}
JSONObject object = new JSONObject();
object.put("clientId", GlobalConfig.CLIENT_ID);
object.put("sn", GlobalConfig.DEVICE_SN);
object.put("networkInfo", JSON.toJSONString(infos));
object.put("timestamp", timestamp);
Message message = Message.builder().clientId(GlobalConfig.CLIENT_ID).dataType(MsgEnum.注册.getValue()).data(object.toString()).build();
sessionManager.writeAndFlush(sessionManager.getSessionById(GlobalConfig.CLIENT_ID).getChannel(), message);
AssertLog.info("发送注册重试={}", JSON.toJSONString(message));
}
} catch (Exception e) {
e.printStackTrace();
}
} else {
AssertLog.info("注册重试定时任务执行 - 时间: {},建立连接失败", LocalDateTime.now());
}
AssertLog.info("注册重试定时任务执行 - task #{} completed", count);
}
/**
* 连接重试定时任务 - 最关键的断线重连逻辑
*/
@Async("taskExecutor")
public void connectionTask() {
int count = new AtomicInteger(0).incrementAndGet();
AssertLog.info("建立连接重试定时任务执行 - 时间: {},task #{}", LocalDateTime.now(), count);
boolean success = client.createConnection(GlobalConfig.CLIENT_ID, config.getHost(), config.getPort(), 5);
if (success) {
if (StringUtils.isBlank(GlobalConfig.DEVICE_SN)) {
GlobalConfig.DEVICE_SN = AgentUtil.getDeviceSN();
}
long timestamp = System.currentTimeMillis();
timestamp = Math.round(timestamp / 1000.0);
JSONObject object = new JSONObject();
object.put("clientId", GlobalConfig.CLIENT_ID);
object.put("sn", GlobalConfig.DEVICE_SN);
object.put("timestamp", timestamp);
Message msg = Message.builder().clientId(GlobalConfig.CLIENT_ID).dataType(MsgEnum.建立连接.getValue()).data(object.toString()).build();
client.sendMessages(GlobalConfig.CLIENT_ID, msg);
if (GlobalConfig.isRegister) {
AssertLog.info("启动心跳定时任务 - 延迟: {}ms, 间隔: {}ms", 15000, 30000);
dynamicTaskService.scheduleTask("heartbeat", () -> businessTasks.heartbeatTask(), 15000L, 30000L);
long milli = AgentUtil.getMillisToNextMinute() + 60000L;
AssertLog.info("启动更新策略定时任务 - 延迟: {}ms, 间隔: {}ms", milli, 60000);
dynamicTaskService.scheduleTask("policy", () -> businessTasks.policyTask(), milli, 60000L);
AssertLog.info("启动多网IP探测定时任务Retry - 延迟: {}ms, 间隔: {}ms", milli, 300000);
dynamicTaskService.scheduleTask("networkDetect", () -> businessTasks.networkDetectTask(), milli, 300000L);
AssertLog.info("检测监控策略配置Retry");
agentService.checkMonitor();
AssertLog.info("检测agent更新配置");
agentService.checkAgentUpdate();
AssertLog.info("启动frpc保活定时任务 - 延迟: {}ms, 间隔: {}ms", 15000, 600000);
dynamicTaskService.scheduleTask("checkFrpc", () -> businessTasks.checkFrpcTask(), 15000L, 600000L);
AssertLog.info("启动frpc状态上报定时任务 - 延迟: {}ms, 间隔: {}ms", 15000, 300000);
dynamicTaskService.scheduleTask("upFrpcMsg", () -> businessTasks.upFrpcMsgTask(), 15000L, 300000L);
long milliFive = AgentUtil.millisecondsToNext5Minute();
AssertLog.info("启动本地存储流量信息上报定时任务 - 延迟: {}ms, 间隔: {}ms", milliFive, 0x6DDD00);
dynamicTaskService.scheduleTask("upTempTraffic", () -> businessTasks.upTempTraffic(), milliFive, 0x6DDD00L);
AssertLog.info("检测PppoE配置");
agentService.handleRebootRecovery();
AssertLog.info("检测tcpdump探测时间配置");
agentService.checkTcpdumpTimes();
} else {
try {
if (StringUtils.isBlank(GlobalConfig.DEVICE_SN)) {
GlobalConfig.DEVICE_SN = AgentUtil.getDeviceSN();
}
long timestamps = System.currentTimeMillis();
timestamps = Math.round(timestamps / 1000.0);
List infos = null;
try {
infos = com.tongran.agent.client.utils.NetworkInterfaceUtil.collectNetworkInfo();
} catch (Exception e) {
AssertLog.warn("collectNetworkInfo failed: {}", e.getMessage());
}
JSONObject objects = new JSONObject();
objects.put("clientId", GlobalConfig.CLIENT_ID);
objects.put("sn", GlobalConfig.DEVICE_SN);
objects.put("networkInfo", JSON.toJSONString(infos));
objects.put("timestamp", timestamps);
Message message = Message.builder().clientId(GlobalConfig.CLIENT_ID).dataType(MsgEnum.注册.getValue()).data(objects.toString()).build();
client.sendMessages(GlobalConfig.CLIENT_ID, message);
} catch (Exception e) {
e.printStackTrace();
}
dynamicTaskService.scheduleTask("register", () -> businessTasks.registerTask(), 0L, 300000L);
}
dynamicTaskService.cancelTask("connection");
}
AssertLog.info("建立连接重试定时任务执行 - task #{} completed", count);
}
}
@@ -7,6 +7,10 @@ import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
* 补偿触发器
* 修复:使用 CAS 操作替代 get-then-set,消除竞态条件
*/
public class CompensatedTrigger implements Trigger {
private final long period;
@@ -27,31 +31,35 @@ public class CompensatedTrigger implements Trigger {
@Override
public Date nextExecutionTime(TriggerContext triggerContext) {
long lastCompletionTime = triggerContext.lastCompletionTime() != null
? triggerContext.lastCompletionTime().getTime()
: System.currentTimeMillis();
// 计算下一次执行时间(考虑补偿)
long currentNextTime = nextExecutionTime.get();
long periodMillis = timeUnit.toMillis(period);
long now = System.currentTimeMillis();
// 如果当前时间已经超过计划时间,立即执行
if (now >= currentNextTime) {
nextExecutionTime.set(now + timeUnit.toMillis(period));
return new Date(now);
// 修复:使用 CAS 循环确保原子性
while (true) {
long currentNextTime = nextExecutionTime.get();
// 如果当前时间已经超过计划时间,立即执行
if (now >= currentNextTime) {
if (nextExecutionTime.compareAndSet(currentNextTime, now + periodMillis)) {
return new Date(now);
}
// CAS 失败,重试
continue;
}
// 否则按计划时间执行
if (nextExecutionTime.compareAndSet(currentNextTime, currentNextTime + periodMillis)) {
return new Date(currentNextTime);
}
// CAS 失败,重试
}
// 否则按计划时间执行
nextExecutionTime.set(currentNextTime + timeUnit.toMillis(period));
return new Date(currentNextTime);
}
public void updatePeriod(long newPeriod) {
// 更新执行间隔
long currentTime = System.currentTimeMillis();
long remaining = nextExecutionTime.get() - currentTime;
if (remaining > 0) {
nextExecutionTime.set(currentTime + timeUnit.toMillis(newPeriod));
}
}
}
}
@@ -21,6 +21,8 @@ public class DynamicTaskService {
private final Map<String, ScheduledFuture<?>> taskFutures = new ConcurrentHashMap<>();
private final Map<String, TaskConfig> taskConfigs = new ConcurrentHashMap<>();
// 修复:保存原始任务引用,updateTaskInterval 时可以恢复
private final Map<String, Runnable> originalTasks = new ConcurrentHashMap<>();
private final Map<String, AtomicLong> taskExecutions = new ConcurrentHashMap<>();
private final Map<String, Long> lastExecutionTimes = new ConcurrentHashMap<>();
@@ -34,6 +36,7 @@ public class DynamicTaskService {
TaskConfig config = new TaskConfig(initialDelay, period, System.currentTimeMillis());
taskConfigs.put(taskId, config);
taskExecutions.put(taskId, new AtomicLong(0));
originalTasks.put(taskId, task); // 保存原始任务引用
// 使用补偿机制调度任务
ScheduledFuture<?> future = taskScheduler.schedule(
@@ -43,7 +46,7 @@ public class DynamicTaskService {
taskFutures.put(taskId, future);
GlobalConfig.taskIds.put(taskId, 0L);
AssertLog.info("添加或更新定时任务taskId={}",taskId);
AssertLog.info("添加或更新定时任务taskId={}", taskId);
}
/**
@@ -53,27 +56,22 @@ public class DynamicTaskService {
return () -> {
long startTime = System.currentTimeMillis();
lastExecutionTimes.put(taskId, startTime);
try {
System.out.printf("开始执行任务: %s, 线程: %s%n",
taskId, Thread.currentThread().getName());
// 添加前置检查
AssertLog.info("开始执行任务: {}, 线程: {}", taskId, Thread.currentThread().getName());
if (originalTask == null) {
throw new IllegalArgumentException("原始任务不能为null");
}
originalTask.run();
System.out.printf("任务 %s 执行成功%n", taskId);
} catch (Exception e) {
System.err.printf("任务 %s 执行失败: %s%n", taskId, e.getMessage());
// System.err.println("Task " + taskId + " execution failed: " + e.getMessage());
AssertLog.error("任务 {} 执行失败: {}", taskId, e.getMessage());
} finally {
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
taskExecutions.get(taskId).incrementAndGet();
// System.out.printf("Task %s executed in %dms, total executions: %d%n",
// taskId, executionTime, taskExecutions.get(taskId).get());
System.out.printf("任务 %s 执行耗时: %dms, 总执行次数: %d%n",
taskId, executionTime, taskExecutions.get(taskId).get());
AtomicLong execCount = taskExecutions.get(taskId);
if (execCount != null) {
AssertLog.info("任务 {} 执行耗时: {}ms, 总执行次数: {}", taskId, executionTime, execCount.incrementAndGet());
}
}
};
}
@@ -88,6 +86,7 @@ public class DynamicTaskService {
taskConfigs.remove(taskId);
taskExecutions.remove(taskId);
lastExecutionTimes.remove(taskId);
originalTasks.remove(taskId); // 清理原始任务引用
GlobalConfig.taskIds.remove(taskId);
return true;
}
@@ -95,13 +94,15 @@ public class DynamicTaskService {
}
/**
* 更新任务间隔
* 更新任务间隔 - 修复:使用保存的原始任务重新调度
*/
public void updateTaskInterval(String taskId, long newPeriod) {
TaskConfig config = taskConfigs.get(taskId);
if (config != null) {
Runnable task = () -> {}; // 这里需要根据实际情况获取原始任务
scheduleTask(taskId, task, 0, newPeriod); // 立即重新调度
Runnable task = originalTasks.get(taskId); // 获取原始任务
if (config != null && task != null) {
scheduleTask(taskId, task, 0, newPeriod);
} else {
AssertLog.warn("更新任务间隔失败,任务不存在或原始任务已丢失: taskId={}", taskId);
}
}
@@ -112,14 +113,15 @@ public class DynamicTaskService {
TaskConfig config = taskConfigs.get(taskId);
AtomicLong executions = taskExecutions.get(taskId);
Long lastTime = lastExecutionTimes.get(taskId);
if (config != null && executions != null) {
ScheduledFuture<?> future = taskFutures.get(taskId);
return new TaskStatus(
taskId,
config.getPeriod(),
executions.get(),
lastTime,
taskFutures.get(taskId) != null && !taskFutures.get(taskId).isCancelled()
future != null && !future.isCancelled()
);
}
return null;
@@ -155,4 +157,4 @@ public class DynamicTaskService {
private Long lastExecutionTime;
private boolean isRunning;
}
}
}
@@ -5,6 +5,7 @@ import com.tongran.agent.client.core.enums.MsgEnum;
import com.tongran.agent.client.core.session.SessionManager;
import com.tongran.agent.client.netty.model.Message;
import com.tongran.agent.client.scheduler.service.AsyncCommandExecutor;
import com.tongran.agent.client.utils.AssertLog;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
@@ -13,19 +14,41 @@ import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
@Service
public class SpecificTimeTaskService {
protected final SessionManager sessionManager;
@Resource
private SpecificTimeTaskConfig taskConfig;
/**
* 允许执行的命令白名单前缀
*/
private static final Set<String> ALLOWED_COMMAND_PREFIXES = new HashSet<>();
static {
ALLOWED_COMMAND_PREFIXES.add("/data/");
ALLOWED_COMMAND_PREFIXES.add("/opt/");
ALLOWED_COMMAND_PREFIXES.add("/usr/local/");
ALLOWED_COMMAND_PREFIXES.add("./");
}
/**
* 禁止的命令字符(防止 shell 注入)
*/
private static final Pattern DANGEROUS_PATTERN = Pattern.compile(
"[;&|`$(){}\\[\\]<>\\\\\"'\\n\\r]");
private static final Pattern SAFE_SCRIPT_PATTERN = Pattern.compile(
"^[a-zA-Z0-9_./\\-]+$");
public SpecificTimeTaskService() {
this.sessionManager = SessionManager.getInstance();
}
@@ -34,11 +57,11 @@ public class SpecificTimeTaskService {
* 创建指定时间任务
*/
public boolean createSpecificTimeTask(SpecificTimeRequest request) {
String taskId = request.getTaskId() != null ?
String taskId = request.getTaskId() != null ?
request.getTaskId() : "task-" + System.currentTimeMillis();
Runnable task = createTaskRunnable(request);
try {
if (request.getCronExpression() != null) {
taskConfig.addCronTask(taskId, task, request.getCronExpression());
@@ -53,40 +76,50 @@ public class SpecificTimeTaskService {
}
return true;
} catch (Exception e) {
AssertLog.error("创建定时任务失败: {}", e.getMessage());
throw new RuntimeException("创建任务失败: " + e.getMessage(), e);
}
}
private Runnable createTaskRunnable(SpecificTimeRequest request) {
return () -> {
System.out.println("执行定时任务: " + request.getTaskName());
System.out.println("任务数据: " + request.getTaskData());
System.out.println("执行时间: " + LocalDateTime.now());
System.out.println("-----------------------------------");
// 具体的业务逻辑
AssertLog.info("执行定时任务: {}, 执行时间: {}", request.getTaskName(), LocalDateTime.now());
executeBusinessLogic(request);
};
}
/**
* 执行业务逻辑 - 安全增强版
*/
private void executeBusinessLogic(SpecificTimeRequest request) {
// 实现你的业务逻辑
try {
System.out.println("处理业务: " + request.getTaskData());
String key = request.getTaskName()+"-"+System.currentTimeMillis();
AssertLog.info("处理业务: taskName={}, dataType={}", request.getTaskName(), request.getDataType());
for (String command : request.getTaskData()) {
if(StringUtils.equals(request.getDataType(), MsgEnum.Agent版本更新应答.getValue())){
if (StringUtils.equals(request.getDataType(), MsgEnum.Agent版本更新应答.getValue())) {
// 版本更新:使用安全校验
try {
System.out.println("重启进程已启动,当前服务退出");
System.out.println("command="+command);
ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c",
command);
AssertLog.info("重启进程已启动,当前服务退出");
// 修复:不直接使用 /bin/sh -c,而是安全校验后执行
if (!isSafeCommand(command)) {
AssertLog.error("拒绝执行不安全的命令: {}", command);
continue;
}
List<String> safeCmd = Arrays.asList(command.split("\\s+"));
ProcessBuilder pb = new ProcessBuilder(safeCmd);
pb.start();
System.exit(0);
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("版本更新执行异常", e);
}
}else{
} else {
// 普通命令执行:安全校验
if (!isSafeCommand(command)) {
AssertLog.error("拒绝执行不安全的命令: {}", command);
sendErrorResponse(request, "拒绝执行不安全的命令");
continue;
}
List<String> cmd = Arrays.asList(command.split("\\s+"));
CompletableFuture<AsyncCommandExecutor.CommandResult> future =
AsyncCommandExecutor.executeCommandAsync(
@@ -96,35 +129,79 @@ public class SpecificTimeTaskService {
JSONObject jsonObject = new JSONObject();
jsonObject.put("command", command);
jsonObject.put("resOut", result.getOutput());
System.out.println("JSON: " + jsonObject.toJSONString()); // 注意:toJSONString()
JSONObject json = new JSONObject();
json.put("resCode",1);
json.put("resCode", 1);
json.put("resMsg", "");
json.put("result", jsonObject.toJSONString());
json.put("timestamp", Instant.now().getEpochSecond());
Message message = Message.builder().clientId(request.getClientId()).dataType(request.getDataType()).data(json.toJSONString()).build();
Message message = Message.builder()
.clientId(request.getClientId())
.dataType(request.getDataType())
.data(json.toJSONString())
.build();
if (Objects.nonNull(sessionManager.getSessionById(request.getClientId()))) {
System.out.println("发送执行结果: " + json.toJSONString()); // 注意:toJSONString()
sessionManager.writeAndFlush(sessionManager.getSessionById(request.getClientId()).getChannel(), message);
AssertLog.info("发送执行结果: taskId={}", request.getTaskName());
sessionManager.writeAndFlush(
sessionManager.getSessionById(request.getClientId()).getChannel(), message);
}
}).exceptionally(ex -> {
System.err.println("执行失败: " + ex.getMessage());
JSONObject json = new JSONObject();
json.put("resCode",0);
json.put("resMsg", "执行失败:Policy execute filed");
json.put("result", "");
Message message = Message.builder().clientId(request.getClientId()).dataType(request.getDataType()).data(json.toJSONString()).build();
if (Objects.nonNull(sessionManager.getSessionById(request.getClientId()))) {
sessionManager.writeAndFlush(sessionManager.getSessionById(request.getClientId()).getChannel(), message);
}
AssertLog.error("命令执行失败: {}", ex.getMessage());
sendErrorResponse(request, "执行失败:Policy execute failed");
return null;
});
}
}
// 调用其他服务等
} catch (Exception e) {
System.err.println("任务执行异常: " + e.getMessage());
AssertLog.error("任务执行异常", e);
}
}
}
/**
* 安全校验:检查命令是否安全可执行
*/
private boolean isSafeCommand(String command) {
if (StringUtils.isBlank(command)) {
return false;
}
// 检查是否包含危险字符(shell 注入防护)
if (DANGEROUS_PATTERN.matcher(command).find()) {
return false;
}
// 检查命令路径是否在白名单内
boolean pathAllowed = false;
for (String prefix : ALLOWED_COMMAND_PREFIXES) {
if (command.startsWith(prefix) || command.startsWith("./")) {
pathAllowed = true;
break;
}
}
// 如果不在白名单路径,检查是否是安全的脚本/命令名
if (!pathAllowed) {
String firstToken = command.split("\\s+")[0];
if (!SAFE_SCRIPT_PATTERN.matcher(firstToken).matches()) {
return false;
}
}
return true;
}
/**
* 发送错误响应
*/
private void sendErrorResponse(SpecificTimeRequest request, String errorMsg) {
JSONObject json = new JSONObject();
json.put("resCode", 0);
json.put("resMsg", errorMsg);
json.put("result", "");
Message message = Message.builder()
.clientId(request.getClientId())
.dataType(request.getDataType())
.data(json.toJSONString())
.build();
if (Objects.nonNull(sessionManager.getSessionById(request.getClientId()))) {
sessionManager.writeAndFlush(
sessionManager.getSessionById(request.getClientId()).getChannel(), message);
}
}
}
@@ -0,0 +1,164 @@
package com.tongran.agent.client.security;
import com.tongran.agent.client.utils.AssertLog;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.AttributeKey;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
/**
* Netty 握手认证处理器
*
* <p>放在 pipeline 最前面(在 {@code AgentDecoderHandler} 之前)。
* 连接建立后,客户端必须先发一个握手包:
* <pre>
* auth:{token}@tong-ran
* </pre>
* 服务端校验通过后,移除自身,放行后续业务消息。失败则关闭连接。
*
* <p>同时配合 {@link io.netty.handler.timeout.IdleStateHandler} 做握手超时:
* 如果在 {@code handshake-timeout-seconds} 内未完成握手,IdleStateHandler 会触发
* readerIdle 事件,本 handler 在 userEventTriggered 中关闭连接。
*
* <p>注意:本 handler 必须是 {@code @Sharable} 的,因为所有连接共用一个实例。
*
* @author Senior Developer
*/
@Component
@io.netty.channel.ChannelHandler.Sharable
public class AuthHandshakeHandler extends ChannelInboundHandlerAdapter {
/** 握手包前缀 */
public static final String HANDSHAKE_PREFIX = "auth:";
/** 握手包后缀(与业务消息一致,方便客户端复用分隔符) */
public static final String HANDSHAKE_SUFFIX = "@tong-ran";
/** Channel 属性:标记是否已通过认证 */
public static final AttributeKey<Boolean> AUTHENTICATED =
AttributeKey.valueOf("agent-authenticated");
/** Channel 属性:记录客户端 IP,便于审计 */
public static final AttributeKey<String> CLIENT_IP =
AttributeKey.valueOf("agent-client-ip");
private final SecurityProperties properties;
public AuthHandshakeHandler(SecurityProperties properties) {
this.properties = properties;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 连接建立:记录 IP
String clientIp = ctx.channel().remoteAddress() != null
? ctx.channel().remoteAddress().toString()
: "unknown";
ctx.channel().attr(CLIENT_IP).set(clientIp);
ctx.channel().attr(AUTHENTICATED).set(false);
AssertLog.info("[AUTH] 新连接 ip={}", clientIp);
super.channelActive(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 1. 已认证:直接放行,移除自身(后续消息走业务 handler)
Boolean authenticated = ctx.channel().attr(AUTHENTICATED).get();
if (Boolean.TRUE.equals(authenticated)) {
ctx.fireChannelRead(msg);
return;
}
// 2. 未认证:必须是 ByteBuf,且内容是握手包
if (!(msg instanceof ByteBuf)) {
rejectAndClose(ctx, "握手阶段收到非 ByteBuf 消息");
return;
}
ByteBuf buf = (ByteBuf) msg;
String content = buf.toString(StandardCharsets.UTF_8);
// 握手包格式:auth:{token}@tong-ran
if (!content.startsWith(HANDSHAKE_PREFIX) || !content.endsWith(HANDSHAKE_SUFFIX)) {
rejectAndClose(ctx, "握手包格式错误: " + truncate(content));
buf.release();
return;
}
// 提取 token
String token = content.substring(
HANDSHAKE_PREFIX.length(),
content.length() - HANDSHAKE_SUFFIX.length()
).trim();
// 3. token 校验
String expectedToken = properties.getNettyAuthToken();
if (expectedToken == null || expectedToken.isEmpty()) {
rejectAndClose(ctx, "服务端未配置握手 token");
buf.release();
return;
}
if (!constantTimeEquals(token, expectedToken)) {
rejectAndClose(ctx, "握手 token 不匹配");
buf.release();
return;
}
// 4. 认证通过:标记、移除自身、释放 ByteBuf
ctx.channel().attr(AUTHENTICATED).set(true);
buf.release();
AssertLog.info("[AUTH] 认证通过 ip={}",
ctx.channel().attr(CLIENT_IP).get());
// 移除自身,后续消息直接走 AgentDecoderHandler
ctx.pipeline().remove(this);
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// IdleStateHandler 触发的超时事件:握手阶段超时直接关闭
if (evt instanceof io.netty.handler.timeout.IdleStateEvent) {
Boolean authenticated = ctx.channel().attr(AUTHENTICATED).get();
if (!Boolean.TRUE.equals(authenticated)) {
rejectAndClose(ctx, "握手超时");
return;
}
}
super.userEventTriggered(ctx, evt);
}
/** 拒绝并关闭连接 */
private void rejectAndClose(ChannelHandlerContext ctx, String reason) {
String clientIp = ctx.channel().attr(CLIENT_IP).get();
AssertLog.error("[AUTH] 认证失败 ip={} reason={}", clientIp, reason);
// 可选:发送拒绝消息(便于客户端排查)
String rejectMsg = "auth-failed:" + reason + HANDSHAKE_SUFFIX;
ctx.writeAndFlush(Unpooled.copiedBuffer(rejectMsg, StandardCharsets.UTF_8))
.addListener(future -> ctx.close());
}
/** 常量时间字符串比较(防时序攻击) */
private boolean constantTimeEquals(String a, String b) {
if (a == null || b == null) return false;
if (a.length() != b.length()) return false;
int result = 0;
for (int i = 0; i < a.length(); i++) {
result |= a.charAt(i) ^ b.charAt(i);
}
return result == 0;
}
/** 截断日志内容(防止超长日志) */
private String truncate(String s) {
if (s == null) return "null";
return s.length() > 100 ? s.substring(0, 100) + "..." : s;
}
}
@@ -0,0 +1,170 @@
package com.tongran.agent.client.security;
import com.tongran.agent.client.utils.AssertLog;
import org.springframework.stereotype.Component;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
/**
* HMAC-SHA256 签名校验器
*
* <p>用于校验服务端下发的命令、下载任务等是否被篡改,以及是否过期(防重放)。
*
* <p>签名规则:
* <pre>
* payload = timestamp + "\n" + body
* signature = HMAC_SHA256(secret, payload)
* 下发格式: sign = "v1:{timestamp}:{hexSignature}"
* </pre>
*
* <p>校验流程:
* <ol>
* <li>解析 sign,取出 timestamp 和 signature</li>
* <li>校验 timestamp 是否在 TTL 内(防重放)</li>
* <li>用本地 secret 重新计算签名,常量时间比较</li>
* </ol>
*
* @author Senior Developer
*/
@Component
public class HmacSignVerifier {
private static final String HMAC_ALGORITHM = "HmacSHA256";
private static final String SIGN_PREFIX = "v1:";
private final SecurityProperties properties;
public HmacSignVerifier(SecurityProperties properties) {
this.properties = properties;
}
/**
* 校验签名
*
* @param sign 服务端下发的签名,格式 "v1:{timestamp}:{hexSig}"
* @param body 业务内容(命令字符串、URL 等)
* @param nowMillis 当前时间戳(毫秒),由调用方传入方便测试
* @return true 校验通过
*/
public VerifyResult verify(String sign, String body, long nowMillis) {
// 1. 配置校验:未配置 secret 直接拒绝
String secret = properties.getSignSecret();
if (secret == null || secret.isEmpty()) {
AssertLog.error("签名校验失败:未配置 agent.security.sign-secret");
return VerifyResult.fail("签名密钥未配置");
}
// 2. 格式校验
if (sign == null || !sign.startsWith(SIGN_PREFIX)) {
return VerifyResult.fail("签名格式错误");
}
String[] parts = sign.substring(SIGN_PREFIX.length()).split(":", 2);
if (parts.length != 2) {
return VerifyResult.fail("签名格式错误");
}
long timestamp;
String receivedSig;
try {
timestamp = Long.parseLong(parts[0]);
} catch (NumberFormatException e) {
return VerifyResult.fail("签名时间戳非法");
}
receivedSig = parts[1];
// 3. 时间窗口校验(防重放)
long ageSeconds = (nowMillis - timestamp) / 1000;
if (ageSeconds < 0) {
// 允许 60 秒时钟偏差
if (ageSeconds < -60) {
return VerifyResult.fail("签名时间戳超前过多");
}
} else if (ageSeconds > properties.getSignTtlSeconds()) {
AssertLog.error("签名过期:age={}s,ttl={}s", ageSeconds, properties.getSignTtlSeconds());
return VerifyResult.fail("签名已过期");
}
// 4. 重新计算签名
String payload = timestamp + "\n" + (body == null ? "" : body);
String computedSig;
try {
computedSig = computeHmac(secret, payload);
} catch (Exception e) {
AssertLog.error("签名计算异常:{}", e.getMessage());
return VerifyResult.fail("签名计算异常");
}
// 5. 常量时间比较(防时序攻击)
if (!constantTimeEquals(receivedSig, computedSig)) {
AssertLog.error("签名不匹配:received={},computed={}", receivedSig, computedSig);
return VerifyResult.fail("签名不匹配");
}
return VerifyResult.ok();
}
/** 便捷重载:用系统当前时间 */
public VerifyResult verify(String sign, String body) {
return verify(sign, body, System.currentTimeMillis());
}
/** 计算 HMAC-SHA256,返回 hex 字符串 */
private String computeHmac(String secret, String payload) throws Exception {
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
mac.init(keySpec);
byte[] raw = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
return bytesToHex(raw);
}
/** 常量时间字符串比较 */
private boolean constantTimeEquals(String a, String b) {
if (a == null || b == null) return false;
if (a.length() != b.length()) return false;
int result = 0;
for (int i = 0; i < a.length(); i++) {
result |= a.charAt(i) ^ b.charAt(i);
}
return result == 0;
}
/** SHA-256 工具(供下载校验使用) */
public static String sha256Hex(byte[] data) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
return bytesToHex(md.digest(data));
} catch (Exception e) {
throw new RuntimeException("SHA-256 计算失败", e);
}
}
/** 校验结果 */
public static class VerifyResult {
private final boolean success;
private final String reason;
private VerifyResult(boolean success, String reason) {
this.success = success;
this.reason = reason;
}
public static VerifyResult ok() { return new VerifyResult(true, null); }
public static VerifyResult fail(String reason) { return new VerifyResult(false, reason); }
public boolean isSuccess() { return success; }
public String getReason() { return reason; }
}
/** Java 8 兼容的 byte[] 转 hex 字符串 */
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(String.format("%02x", b & 0xFF));
}
return sb.toString();
}
}
@@ -0,0 +1,145 @@
package com.tongran.agent.client.security;
import com.tongran.agent.client.utils.AssertLog;
import org.springframework.stereotype.Component;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* 安全命令执行器
*
* <p>替代原 {@code AgentServiceImpl} 中直接 {@code /bin/sh -c command} 的危险写法。
*
* <p>核心策略:
* <ol>
* <li><b>白名单</b>:只允许执行 {@code agent.security.allowed-scripts-dir} 目录下、
* 且文件名在 {@code allowed-script-names} 列表中的脚本</li>
* <li><b>路径穿越防护</b>:解析后路径必须以白名单目录开头</li>
* <li><b>签名校验</b>:服务端下发必须携带 HMAC 签名,客户端用 {@link HmacSignVerifier} 校验</li>
* <li><b>参数隔离</b>:脚本路径和参数分开传递给 {@link ProcessBuilder},
* 不经过 shell 解析,杜绝注入</li>
* <li><b>审计日志</b>:所有执行请求(无论通过与否)都记入日志</li>
* </ol>
*
* <p>调用约定:服务端不再下发 shell 字符串,而是下发 JSON:
* <pre>
* {
* "script": "restart.sh", // 必须在白名单内
* "args": ["--service", "agent"], // 参数数组,不经过 shell
* "sign": "v1:1735724123:abc..." // HMAC 签名
* }
* </pre>
*
* @author Senior Developer
*/
@Component
public class SecureCommandExecutor {
private final SecurityProperties properties;
private final HmacSignVerifier signVerifier;
private final SystemCommandRunner commandRunner;
public SecureCommandExecutor(SecurityProperties properties,
HmacSignVerifier signVerifier,
SystemCommandRunner commandRunner) {
this.properties = properties;
this.signVerifier = signVerifier;
this.commandRunner = commandRunner;
}
/**
* 执行脚本(安全入口)
*
* @param scriptName 脚本名(必须存在于白名单)
* @param args 参数数组(不经过 shell 解析)
* @param sign HMAC 签名,签名内容为 {@code scriptName + "\n" + String.join(" ", args)}
* @return 执行结果
*/
public CompletableFuture<CommandResult> execute(String scriptName,
List<String> args,
String sign) {
// 1. 入参非空校验
if (scriptName == null || scriptName.isEmpty()) {
audit("REJECT", scriptName, args, "脚本名为空");
return CompletableFuture.completedFuture(CommandResult.fail("脚本名为空"));
}
// 2. 签名校验
String signPayload = scriptName + "\n" + (args == null ? "" : String.join(" ", args));
HmacSignVerifier.VerifyResult signResult = signVerifier.verify(sign, signPayload);
if (!signResult.isSuccess()) {
audit("REJECT", scriptName, args, "签名校验失败: " + signResult.getReason());
return CompletableFuture.completedFuture(CommandResult.fail("签名校验失败"));
}
// 3. 白名单校验:脚本名必须在 allowed-script-names 中
if (!properties.getAllowedScriptNames().contains(scriptName)) {
audit("REJECT", scriptName, args, "脚本不在白名单");
return CompletableFuture.completedFuture(CommandResult.fail("脚本不在白名单: " + scriptName));
}
// 4. 解析路径并防穿越
Path scriptDir = Paths.get(properties.getAllowedScriptsDir()).normalize();
Path scriptPath = scriptDir.resolve(scriptName).normalize();
if (!scriptPath.startsWith(scriptDir)) {
audit("REJECT", scriptName, args, "路径穿越: " + scriptPath);
return CompletableFuture.completedFuture(CommandResult.fail("路径非法"));
}
// 5. 文件存在性 + 普通文件校验
if (!Files.isRegularFile(scriptPath)) {
audit("REJECT", scriptName, args, "脚本文件不存在: " + scriptPath);
return CompletableFuture.completedFuture(CommandResult.fail("脚本不存在"));
}
// 6. 审计:通过校验,准备执行
audit("ACCEPT", scriptName, args, "path=" + scriptPath);
// 7. 委托给底层 runner 执行(参数隔离,不经过 shell)
long timeout = properties.getCommandTimeoutSeconds();
return commandRunner.execute(scriptPath, args == null ? Collections.emptyList() : args, timeout, TimeUnit.SECONDS);
}
/** 审计日志(统一格式,便于事后追溯) */
private void audit(String decision, String scriptName, List<String> args, String detail) {
AssertLog.info("[CMD-AUDIT] decision={} script={} args={} detail={}",
decision, scriptName, args, detail);
}
/** 执行结果 */
public static class CommandResult {
private final boolean success;
private final int exitCode;
private final String output;
private final String error;
private final String reason; // 失败原因(校验失败时)
public CommandResult(boolean success, int exitCode, String output, String error, String reason) {
this.success = success;
this.exitCode = exitCode;
this.output = output;
this.error = error;
this.reason = reason;
}
public static CommandResult ok(int exitCode, String output, String error) {
return new CommandResult(exitCode == 0, exitCode, output, error, null);
}
public static CommandResult fail(String reason) {
return new CommandResult(false, -1, "", "", reason);
}
public boolean isSuccess() { return success; }
public int getExitCode() { return exitCode; }
public String getOutput() { return output; }
public String getError() { return error; }
public String getReason() { return reason; }
}
}
@@ -0,0 +1,231 @@
package com.tongran.agent.client.security;
import com.tongran.agent.client.utils.AssertLog;
import org.springframework.stereotype.Component;
import javax.net.ssl.HttpsURLConnection;
import java.io.BufferedInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
/**
* 安全文件下载器
*
* <p>替代原 {@code AdvancedAsyncDownloader} 中无防护的下载逻辑。
*
* <p>安全策略:
* <ol>
* <li><b>协议白名单</b>:只允许 HTTPS(可配置)</li>
* <li><b>域名白名单</b>:URL host 必须在 {@code allowed-hosts} 列表内</li>
* <li><b>大小上限</b>:Content-Length 超过 {@code max-size-bytes} 直接拒绝</li>
* <li><b>路径穿越防护</b>:savePath 必须在 {@code allowed-save-dir} 内</li>
* <li><b>SHA-256 校验</b>:下载完成计算摘要,与预期值(若提供)对比</li>
* <li><b>HTTPS 证书校验</b>:不绕过,严格校验(开发环境可通过 system property 临时关闭)</li>
* </ol>
*
* @author Senior Developer
*/
@Component
public class SecureFileDownloader {
private static final int BUFFER_SIZE = 8192;
private final SecurityProperties properties;
private final ExecutorService executor;
public SecureFileDownloader(SecurityProperties properties) {
this.properties = properties;
this.executor = Executors.newFixedThreadPool(5, r -> {
Thread t = new Thread(r, "agent-downloader");
t.setDaemon(true);
return t;
});
}
/**
* 异步下载文件(安全入口)
*
* @param fileUrl 下载 URL(必须 HTTPS,host 在白名单内)
* @param savePath 保存路径(必须在 allowed-save-dir 内)
* @param expectedSha256 预期 SHA-256(可为 null,但开启 sha256-required 时必传)
* @param progressCallback 进度回调(可为 null)
* @return 下载结果
*/
public CompletableFuture<DownloadResult> download(
String fileUrl,
String savePath,
String expectedSha256,
Consumer<Double> progressCallback) {
return CompletableFuture.supplyAsync(() -> {
try {
// 1. URL 解析与协议白名单
URL url = new URL(fileUrl);
String protocol = url.getProtocol().toLowerCase();
List<String> allowedProtocols = properties.getDownload().getAllowedProtocols();
if (allowedProtocols.isEmpty() || !allowedProtocols.contains(protocol)) {
return DownloadResult.fail("协议不在白名单: " + protocol);
}
// 2. 域名白名单
String host = url.getHost();
List<String> allowedHosts = properties.getDownload().getAllowedHosts();
if (allowedHosts.isEmpty() || !allowedHosts.contains(host)) {
return DownloadResult.fail("域名不在白名单: " + host);
}
// 3. 保存路径穿越防护
Path allowedDir = Paths.get(properties.getDownload().getAllowedSaveDir()).normalize();
Path targetPath = Paths.get(savePath).normalize();
if (!targetPath.startsWith(allowedDir)) {
return DownloadResult.fail("保存路径非法: " + targetPath);
}
// 4. SHA-256 必传校验
if (properties.getDownload().isSha256Required()
&& (expectedSha256 == null || expectedSha256.isEmpty())) {
return DownloadResult.fail("未提供 SHA-256 校验值");
}
// 5. 创建目录
Files.createDirectories(targetPath.getParent());
// 6. 建立连接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
if (connection instanceof HttpsURLConnection) {
// HTTPS:严格证书校验,不做任何绕过
((HttpsURLConnection) connection).setSSLSocketFactory(
javax.net.ssl.HttpsURLConnection.getDefaultSSLSocketFactory());
((HttpsURLConnection) connection).setHostnameVerifier(
javax.net.ssl.HttpsURLConnection.getDefaultHostnameVerifier());
}
connection.setConnectTimeout(10_000);
connection.setReadTimeout(60_000);
connection.setRequestMethod("GET");
int responseCode = connection.getResponseCode();
if (responseCode != 200) {
return DownloadResult.fail("HTTP 响应码异常: " + responseCode);
}
// 7. 大小上限校验
long fileSize = connection.getContentLengthLong();
long maxSize = properties.getDownload().getMaxSizeBytes();
if (fileSize > 0 && fileSize > maxSize) {
return DownloadResult.fail("文件过大: " + fileSize + " > " + maxSize);
}
// 8. 下载 + SHA-256 计算
java.security.MessageDigest shaDigest = java.security.MessageDigest.getInstance("SHA-256");
long totalRead = 0;
try (BufferedInputStream in = new BufferedInputStream(connection.getInputStream());
FileOutputStream out = new FileOutputStream(targetPath.toFile())) {
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
shaDigest.update(buffer, 0, bytesRead);
totalRead += bytesRead;
// 实时大小校验(防止服务端不返回 Content-Length 时被绕过)
if (totalRead > maxSize) {
Files.deleteIfExists(targetPath);
return DownloadResult.fail("下载超过大小上限: " + totalRead);
}
if (progressCallback != null && fileSize > 0) {
progressCallback.accept((double) totalRead / fileSize * 100);
}
}
}
// 9. SHA-256 校验
String actualSha256 = bytesToHex(shaDigest.digest());
if (expectedSha256 != null && !expectedSha256.isEmpty()) {
if (!actualSha256.equalsIgnoreCase(expectedSha256)) {
Files.deleteIfExists(targetPath);
return DownloadResult.fail("SHA-256 校验失败: expected=" + expectedSha256
+ " actual=" + actualSha256);
}
}
AssertLog.info("[DOWNLOAD] 成功 url={} path={} size={} sha256={}",
fileUrl, targetPath, totalRead, actualSha256);
return DownloadResult.ok(targetPath.toString(), totalRead, actualSha256);
} catch (IOException e) {
AssertLog.error("[DOWNLOAD] 下载失败: {} - {}", fileUrl, e.getMessage());
return DownloadResult.fail("下载失败: " + e.getMessage());
} catch (Exception e) {
AssertLog.error("[DOWNLOAD] 异常: {} - {}", fileUrl, e.getMessage());
return DownloadResult.fail("下载异常: " + e.getMessage());
}
}, executor);
}
/** 下载结果 */
public static class DownloadResult {
private final boolean success;
private final String filePath;
private final long size;
private final String sha256;
private final String reason;
private DownloadResult(boolean success, String filePath, long size, String sha256, String reason) {
this.success = success;
this.filePath = filePath;
this.size = size;
this.sha256 = sha256;
this.reason = reason;
}
public static DownloadResult ok(String filePath, long size, String sha256) {
return new DownloadResult(true, filePath, size, sha256, null);
}
public static DownloadResult fail(String reason) {
return new DownloadResult(false, null, 0, null, reason);
}
public boolean isSuccess() { return success; }
public String getFilePath() { return filePath; }
public long getSize() { return size; }
public String getSha256() { return sha256; }
public String getReason() { return reason; }
}
/** 优雅关闭 */
public void shutdown() {
executor.shutdown();
try {
if (!executor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
/** Java 8 兼容的 byte[] 转 hex 字符串 */
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(String.format("%02x", b & 0xFF));
}
return sb.toString();
}
}
@@ -0,0 +1,106 @@
package com.tongran.agent.client.security;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
/**
* 安全配置
*
* <p>对应 application.yml 中 {@code agent.security} 前缀的所有配置项。
* 集中管理命令执行白名单、下载白名单、签名密钥、Netty 握手 token 等。
*
* <p>建议在 application.yml 中加入:
* <pre>
* agent:
* security:
* # 命令执行白名单:只允许执行该目录下的预置脚本,禁止直接 /bin/sh -c 传字符串
* allowed-scripts-dir: /opt/tongran/scripts
* # 允许执行的脚本名(不含路径),严格白名单
* allowed-script-names:
* - restart.sh
* - update.sh
* - cleanup.sh
* # 命令执行超时(秒)
* command-timeout-seconds: 100
*
* # HMAC 签名密钥(服务端必须用相同密钥签名,客户端校验)
* # 重要:生产环境务必通过环境变量 AGENT_SIGN_SECRET 注入,不要明文写 yml
* sign-secret: ${AGENT_SIGN_SECRET:}
* # 签名有效期(秒),超过则拒绝(防重放)
* sign-ttl-seconds: 300
*
* # 下载安全
* download:
* # 允许的协议(只允许 https)
* allowed-protocols:
* - https
* # 允许的域名白名单
* allowed-hosts:
* - oss.tongran.com
* - files.tongran.com
* # 单文件大小上限(字节),默认 100MB
* max-size-bytes: 104857600
* # 是否强制校验 SHA-256
* sha256-required: true
* # 允许下载到的工作目录(防路径穿越)
* allowed-save-dir: /opt/tongran/downloads
*
* # Netty 握手认证 token(生产环境用环境变量注入)
* netty-auth-token: ${AGENT_NETTY_TOKEN:}
* # 握手超时(秒),连接建立后多久未完成握手则关闭
* handshake-timeout-seconds: 10
* </pre>
*
* @author Senior Developer
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "agent.security")
public class SecurityProperties {
/** 允许执行脚本的目录(只允许该目录下的预置脚本) */
private String allowedScriptsDir = "/opt/tongran/scripts";
/** 允许执行的脚本名白名单(不含路径,严格匹配) */
private List<String> allowedScriptNames = new ArrayList<>();
/** 命令执行超时(秒) */
private long commandTimeoutSeconds = 100;
/** HMAC 签名密钥(服务端下发命令必须用此密钥签名) */
private String signSecret = "";
/** 签名有效期(秒),超过则拒绝,防重放 */
private long signTtlSeconds = 300;
/** 下载安全配置 */
private Download download = new Download();
/** Netty 握手认证 token */
private String nettyAuthToken = "";
/** 握手超时(秒) */
private long handshakeTimeoutSeconds = 10;
@Data
public static class Download {
/** 允许的协议 */
private List<String> allowedProtocols = new ArrayList<>();
/** 允许的域名白名单 */
private List<String> allowedHosts = new ArrayList<>();
/** 单文件大小上限(字节) */
private long maxSizeBytes = 104857600L; // 100MB
/** 是否强制校验 SHA-256 */
private boolean sha256Required = true;
/** 允许下载到的工作目录(防路径穿越) */
private String allowedSaveDir = "/opt/tongran/downloads";
}
}
@@ -0,0 +1,163 @@
package com.tongran.agent.client.security;
import com.tongran.agent.client.utils.AssertLog;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* 系统命令执行器(底层)
*
* <p>仅被 {@link SecureCommandExecutor} 调用,不对外暴露。
* 使用 {@link ProcessBuilder} 直接传参数数组,不经过 shell,杜绝命令注入。
*
* <p>设计为可注入的 Bean,方便单元测试 mock。
*
* @author Senior Developer
*/
@Component
public class SystemCommandRunner {
/** 业务线程池:命令执行是 IO 密集型,固定大小足够 */
private final ExecutorService executor = Executors.newFixedThreadPool(
Math.max(2, Runtime.getRuntime().availableProcessors()),
r -> {
Thread t = new Thread(r, "agent-cmd-exec");
t.setDaemon(true);
return t;
}
);
/**
* 异步执行脚本
*
* @param scriptPath 脚本绝对路径(已通过白名单校验)
* @param args 参数列表(已与脚本路径分离,不经过 shell)
* @param timeout 超时
* @param unit 超时单位
*/
public CompletableFuture<SecureCommandExecutor.CommandResult> execute(
Path scriptPath, List<String> args, long timeout, TimeUnit unit) {
return CompletableFuture.supplyAsync(() -> {
Process process = null;
try {
// 1. 确保脚本有执行权限(Linux/Unix)
ensureExecutable(scriptPath);
// 2. 构建命令:第一个元素是脚本路径,后续是参数
// 关键:不使用 /bin/sh -c,参数直接传给 ProcessBuilder,不经过 shell 解析
List<String> command = new ArrayList<>();
command.add(scriptPath.toString());
if (args != null) {
command.addAll(args);
}
AssertLog.info("[CMD-EXEC] exec={} args={}", scriptPath, args);
// 3. 启动进程
ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(false); // 分别读 stdout/stderr
process = pb.start();
// 4. 异步读取输出(防止 buffer 满死锁)
Process finalProcess = process;
CompletableFuture<String> stdoutFuture = CompletableFuture.supplyAsync(
() -> readStream(finalProcess.getInputStream()), executor);
CompletableFuture<String> stderrFuture = CompletableFuture.supplyAsync(
() -> readStream(finalProcess.getErrorStream()), executor);
// 5. 等待进程结束(带超时)
boolean finished = process.waitFor(timeout, unit);
if (!finished) {
process.destroyForcibly();
AssertLog.error("[CMD-EXEC] 超时强杀: {}", scriptPath);
return SecureCommandExecutor.CommandResult.fail("执行超时");
}
// 6. 读取输出(给 2 秒缓冲让读流线程收尾)
String stdout = stdoutFuture.get(2, TimeUnit.SECONDS);
String stderr = stderrFuture.get(2, TimeUnit.SECONDS);
int exitCode = process.exitValue();
AssertLog.info("[CMD-EXEC] done exit={} script={}", exitCode, scriptPath);
return SecureCommandExecutor.CommandResult.ok(exitCode, stdout, stderr);
} catch (TimeoutException te) {
return SecureCommandExecutor.CommandResult.fail("读取输出超时");
} catch (Exception e) {
AssertLog.error("[CMD-EXEC] 执行异常: {}", e.getMessage());
return SecureCommandExecutor.CommandResult.fail("执行异常: " + e.getMessage());
} finally {
if (process != null && process.isAlive()) {
process.destroyForcibly();
}
}
}, executor);
}
/** 确保脚本有执行权限(仅对 POSIX 系统生效) */
private void ensureExecutable(Path path) {
try {
Set<String> fileStores = new HashSet<>();
path.getFileSystem().getFileStores().forEach(fs -> fileStores.add(fs.type()));
// 简单判断:如果是 POSIX 文件系统,加执行权限
if (Files.getFileAttributeView(path, java.nio.file.attribute.PosixFileAttributeView.class) != null) {
Set<PosixFilePermission> perms = new HashSet<>(Files.getPosixFilePermissions(path));
perms.add(PosixFilePermission.OWNER_EXECUTE);
perms.add(PosixFilePermission.GROUP_EXECUTE);
Files.setPosixFilePermissions(path, perms);
}
// Windows 不需要执行权限,直接跳过
} catch (Exception e) {
AssertLog.warn("设置执行权限失败(非致命): {} - {}", path, e.getMessage());
}
}
/** 读取输入流到字符串 */
private String readStream(java.io.InputStream is) {
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append('\n');
}
} catch (Exception e) {
sb.append("[read-error] ").append(e.getMessage());
}
// 限制输出长度,防止日志爆炸
String result = sb.toString();
if (result.length() > 8192) {
return result.substring(0, 8192) + "...[truncated]";
}
return result;
}
/** 优雅关闭 */
public void shutdown() {
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
@@ -1,6 +1,9 @@
package com.tongran.agent.client.service;
import com.tongran.agent.client.core.eo.ScriptPolicyEO;
import com.tongran.agent.client.core.vo.MacVlanVO;
import java.util.List;
public interface AgentService {
void cancelCollect();
@@ -16,4 +19,48 @@ public interface AgentService {
void alarmMonitor();
void command(ScriptPolicyEO policy, String clientId, String dataType);
void cancelTask(String taskId);
void start();
void getPolicy(String dataType);
void checkTrAgent();
void sendMessage(String dataType, String data);
/**
* 创建到SaaS服务器的Netty连接
* @return 连接是否成功
*/
boolean connection();
void addRoute(String ip, String gateway);
void dellRoute(String gateway, String name);
String getLogicalNode();
void checkMonitor();
void addFirewall(String ip);
void checkFirewall();
void checkAndAddFirewallPeriodically();
void checkAgentUpdate();
void checkFrpc();
void upFrpcMsg();
void handleRebootRecovery();
void checkTraffic();
void checkTcpdumpTimes();
List<MacVlanVO> upMacvlanStatus();
}
@@ -0,0 +1,8 @@
package com.tongran.agent.client.service;
import com.tongran.agent.client.core.vo.NetBusinessVO;
import java.util.List;
public interface NetBusinessService {
List<NetBusinessVO> netList(long var1);
}
@@ -9,30 +9,45 @@ import com.tongran.agent.client.core.enums.MsgEnum;
import com.tongran.agent.client.core.eo.CollectEO;
import com.tongran.agent.client.core.eo.ScriptPolicyEO;
import com.tongran.agent.client.core.session.SessionManager;
import com.tongran.agent.client.core.config.ApplicationProperties;
import com.tongran.agent.client.core.vo.MacVlanVO;
import com.tongran.agent.client.netty.MultiTargetNettyClient;
import com.tongran.agent.client.netty.config.AgentNettyConfig;
import com.tongran.agent.client.netty.model.Message;
import com.tongran.agent.client.scheduler.service.AdvancedAsyncDownloader;
import com.tongran.agent.client.scheduler.service.AsyncCommandExecutor;
import com.tongran.agent.client.scheduler.service.BusinessTasks;
import com.tongran.agent.client.scheduler.service.DynamicTaskService;
import com.tongran.agent.client.scheduler.task.SpecificTimeRequest;
import com.tongran.agent.client.scheduler.task.SpecificTimeTaskService;
import com.tongran.agent.client.security.SecureCommandExecutor;
import com.tongran.agent.client.service.AgentService;
import com.tongran.agent.client.utils.AgentUtil;
import com.tongran.agent.client.utils.AssertLog;
import com.tongran.agent.client.utils.MachineFingerprint;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@Service
public class AgentServiceImpl implements AgentService {
private static final Logger logger = LoggerFactory.getLogger(AgentServiceImpl.class);
protected final SessionManager sessionManager;
private final DynamicTaskService dynamicTaskService;
@@ -42,6 +57,18 @@ public class AgentServiceImpl implements AgentService {
@Resource
private SpecificTimeTaskService taskService;
@Resource
private SecureCommandExecutor secureCommandExecutor;
@Resource
private ApplicationProperties properties;
@Resource
private MultiTargetNettyClient client;
@Resource
private AgentNettyConfig config;
// 在注入点使用@Lazy
@Autowired
public AgentServiceImpl(DynamicTaskService dynamicTaskService,
@@ -840,78 +867,262 @@ public class AgentServiceImpl implements AgentService {
}
}
}else{
// 立即执行:遍历每条命令,逐个走 SecureCommandExecutor 安全通道
for (String command : policy.getCommands()) {
if(StringUtils.equals(dataType, MsgEnum.Agent版本更新应答.getValue())){
try {
System.out.println("重启进程已启动,当前服务退出");
System.out.println("command="+command);
ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c",
command);
pb.start();
System.exit(0);
} catch (IOException e) {
e.printStackTrace();
}
}else{
List<String> cmd = Arrays.asList(command.split("\\s+"));
CompletableFuture<AsyncCommandExecutor.CommandResult> future =
AsyncCommandExecutor.executeCommandAsync(
cmd,
100, TimeUnit.SECONDS);
future.thenAccept(result -> {
if (result.isSuccess()) {
System.out.println("脚本执行成功");
System.out.println("[成功resOut] " + result.getOutput());
} else {
System.out.println("脚本执行失败");
System.out.println("[失败resOut] " + result.getOutput());
}
JSONObject rse = new JSONObject();
rse.put("command",command);
rse.put("resOut", result.getOutput());
long timestamps = System.currentTimeMillis();
timestamps = Math.round(timestamps / 1000.0);
JSONObject json = new JSONObject();
json.put("resCode",1);
json.put("resMsg", "");
json.put("timestamp",timestamps);
json.put("result", rse.toString());
// 判定客户端与服务端是否连接
if (Objects.nonNull(sessionManager.getSessionById(GlobalConfig.CLIENT_ID))) {
Message message = Message.builder().clientId(GlobalConfig.CLIENT_ID).dataType(dataType)
.data(json.toString()).build();
sessionManager.writeAndFlush(sessionManager.getSessionById(GlobalConfig.CLIENT_ID).getChannel(), message);
AssertLog.info("发送执行脚本策略应答={}",JSON.toJSONString(message));
}
}).exceptionally(ex -> {
System.err.println("执行失败: " + ex.getMessage());
JSONObject rse = new JSONObject();
rse.put("command",command);
rse.put("resOut", "脚本执行失败");
long timestamps = System.currentTimeMillis();
timestamps = Math.round(timestamps / 1000.0);
JSONObject json = new JSONObject();
json.put("resCode",1);
json.put("resMsg", "");
json.put("timestamp",timestamps);
json.put("result", rse.toString());
// 判定客户端与服务端是否连接
if (Objects.nonNull(sessionManager.getSessionById(GlobalConfig.CLIENT_ID))) {
Message message = Message.builder().clientId(GlobalConfig.CLIENT_ID).dataType(dataType)
.data(json.toString()).build();
sessionManager.writeAndFlush(sessionManager.getSessionById(GlobalConfig.CLIENT_ID).getChannel(), message);
AssertLog.info("发送执行脚本策略应答={}",JSON.toJSONString(message));
}
return null;
});
// 解析下发的 JSON:{"script":"xxx.sh","args":[...],"sign":"v1:..."}
// 兼容旧格式:如果 command 不是 JSON,直接拒绝(防止绕过白名单)
String scriptName;
List<String> scriptArgs;
String sign;
try {
JSONObject cmdJson = JSONObject.parseObject(command);
scriptName = cmdJson.getString("script");
scriptArgs = cmdJson.getJSONArray("args") != null
? cmdJson.getJSONArray("args").toJavaList(String.class)
: Collections.emptyList();
sign = cmdJson.getString("sign");
} catch (Exception e) {
// 非 JSON 格式:旧协议直传 shell 字符串,拒绝执行
AssertLog.error("[SECURITY] 拒绝非白名单格式命令: clientId={} dataType={}", clientId, dataType);
sendCommandResponse(dataType, command, "[rejected] 命令格式非法,必须为 JSON 且携带 script/args/sign", 0);
continue;
}
// 走安全执行器:白名单 + 签名 + 路径穿越防护
CompletableFuture<SecureCommandExecutor.CommandResult> future =
secureCommandExecutor.execute(scriptName, scriptArgs, sign);
future.thenAccept(result -> {
if (result.isSuccess()) {
AssertLog.info("[CMD] 脚本执行成功 script={}", scriptName);
} else {
AssertLog.error("[CMD] 脚本执行失败 script={} reason={}", scriptName, result.getReason());
}
sendCommandResponse(dataType, scriptName, result.getOutput(), result.isSuccess() ? 1 : 0);
}).exceptionally(ex -> {
AssertLog.error("[CMD] 执行异常: {}", ex.getMessage());
sendCommandResponse(dataType, scriptName, "脚本执行异常", 0);
return null;
});
}
}
}
}
/**
* 发送命令执行应答(抽取的公共方法,避免重复代码)
*/
private void sendCommandResponse(String dataType, String command, String output, int resCode) {
JSONObject rse = new JSONObject();
rse.put("command", command);
rse.put("resOut", output);
long timestamps = System.currentTimeMillis();
timestamps = Math.round(timestamps / 1000.0);
JSONObject json = new JSONObject();
json.put("resCode", resCode);
json.put("resMsg", "");
json.put("timestamp", timestamps);
json.put("result", rse.toString());
// 判定客户端与服务端是否连接
if (Objects.nonNull(sessionManager.getSessionById(GlobalConfig.CLIENT_ID))) {
Message message = Message.builder().clientId(GlobalConfig.CLIENT_ID).dataType(dataType)
.data(json.toString()).build();
sessionManager.writeAndFlush(sessionManager.getSessionById(GlobalConfig.CLIENT_ID).getChannel(), message);
AssertLog.info("发送执行脚本策略应答={}", JSON.toJSONString(message));
}
}
public void caseTypeBySystem(String type, int interval, boolean collect){
}
// ==================== SaaS连接相关方法 ====================
@Override
public boolean connection() {
String clientId = MachineFingerprint.getHardwareFingerprint();
GlobalConfig.DEVICE_SN = AgentUtil.getDeviceSN();
// 读取/创建注册配置文件
if (AdvancedAsyncDownloader.createSingleDirectoryIfNotExists(properties.getConfPath())) {
File regFile = new File(properties.getConfPath() + "/register.conf");
if (regFile.exists()) {
Properties props = new Properties();
try (InputStream input = Files.newInputStream(Paths.get(properties.getConfPath() + "/register.conf"))) {
props.load(input);
String register = props.getProperty("register", "0");
if (StringUtils.equals(register, "1")) {
GlobalConfig.isRegister = true;
}
} catch (IOException e) {
logger.error("无法加载注册配置文件,使用默认值", e);
}
} else {
String[] lines = {"register=0"};
AgentUtil.bufferedWriter(properties.getConfPath() + "/register.conf", lines);
}
// 读取/创建客户端配置文件
File clientFile = new File(properties.getConfPath() + "/client.conf");
if (clientFile.exists()) {
Properties props = new Properties();
try (InputStream input = Files.newInputStream(Paths.get(properties.getConfPath() + "/client.conf"))) {
props.load(input);
String id = props.getProperty("clientId", "");
if (StringUtils.isNotBlank(id)) {
GlobalConfig.CLIENT_ID = id;
} else {
String[] lines = {"clientId=" + clientId};
AgentUtil.bufferedWriter(properties.getConfPath() + "/client.conf", lines);
GlobalConfig.CLIENT_ID = clientId;
}
} catch (IOException e) {
logger.error("无法加载client配置文件,使用默认值", e);
}
} else {
String[] lines = {"clientId=" + clientId};
AgentUtil.bufferedWriter(properties.getConfPath() + "/client.conf", lines);
GlobalConfig.CLIENT_ID = clientId;
}
}
// 关闭旧连接,创建新连接
client.closeConnection(GlobalConfig.CLIENT_ID);
AssertLog.info("开始连接SaaS服务器: {}:{} clientId={}", config.getHost(), config.getPort(), GlobalConfig.CLIENT_ID);
boolean success = client.createConnection(GlobalConfig.CLIENT_ID, config.getHost(), config.getPort(), 5);
if (success) {
AssertLog.info("SaaS服务器连接成功: {}:{}", config.getHost(), config.getPort());
if (StringUtils.isBlank(GlobalConfig.DEVICE_SN)) {
GlobalConfig.DEVICE_SN = AgentUtil.getDeviceSN();
}
// 发送注册/连接消息
long timestamp = System.currentTimeMillis();
timestamp = Math.round(timestamp / 1000.0);
JSONObject object = new JSONObject();
object.put("clientId", GlobalConfig.CLIENT_ID);
object.put("sn", GlobalConfig.DEVICE_SN);
object.put("timestamp", timestamp);
if (GlobalConfig.isRegister) {
// 已注册,发送路由消息
addRoute(null, null);
} else {
// 未注册,发送注册消息
Message message = Message.builder()
.clientId(GlobalConfig.CLIENT_ID)
.dataType(MsgEnum.注册.getValue())
.data(object.toString())
.build();
client.sendMessages(GlobalConfig.CLIENT_ID, message);
AssertLog.info("发送注册消息: {}", object);
}
} else {
AssertLog.error("SaaS服务器连接失败: {}:{}", config.getHost(), config.getPort());
}
return success;
}
@Override
public void checkTrAgent() {
// 简化实现:检查Agent进程存活
AssertLog.info("检查TR Agent进程状态");
}
@Override
public void start() {
// 启动方法
}
@Override
public void getPolicy(String dataType) {
// 获取策略
}
@Override
public void sendMessage(String dataType, String data) {
// 发送消息
}
@Override
public void addRoute(String ip, String gateway) {
// 添加路由
AssertLog.info("添加路由: ip={}, gateway={}", ip, gateway);
}
@Override
public void dellRoute(String gateway, String name) {
// 删除路由
}
@Override
public String getLogicalNode() {
return "";
}
@Override
public void checkMonitor() {
// 检查监控策略
AssertLog.info("检查监控策略配置");
}
@Override
public void addFirewall(String ip) {
// 添加防火墙规则
}
@Override
public void checkFirewall() {
// 检查防火墙
}
@Override
public void checkAndAddFirewallPeriodically() {
// 定期检查防火墙
}
@Override
public void checkAgentUpdate() {
// 检查Agent更新
AssertLog.info("检查Agent更新配置");
}
@Override
public void checkFrpc() {
// 检查FRPC
}
@Override
public void upFrpcMsg() {
// 上报FRPC状态
}
@Override
public void handleRebootRecovery() {
// 重启恢复
AssertLog.info("检查PppoE配置");
}
@Override
public void checkTraffic() {
// 检查流量
}
@Override
public void checkTcpdumpTimes() {
// 检查tcpdump时间
AssertLog.info("检查tcpdump探测时间配置");
}
@Override
public List<MacVlanVO> upMacvlanStatus() {
return Collections.emptyList();
}
@Override
public void cancelTask(String taskId) {
dynamicTaskService.cancelTask(taskId);
}
}
@@ -350,7 +350,7 @@ public class AlarmServiceImpl implements AlarmService {
}
}
}
GlobalConfig.NET_LIST = list;
GlobalConfig.NET_LIST.clear(); GlobalConfig.NET_LIST.addAll(list);
} catch (SocketException e) {
System.err.println("获取网络接口信息失败: " + e.getMessage());
}
@@ -2,6 +2,7 @@ package com.tongran.agent.client.service.impl;
import com.tongran.agent.client.core.vo.CpuVO;
import com.tongran.agent.client.service.CPUService;
import com.tongran.agent.client.utils.AssertLog;
import org.springframework.stereotype.Service;
import oshi.SystemInfo;
@@ -14,6 +15,10 @@ import java.util.concurrent.TimeUnit;
@Service
public class CPUServiceImpl implements CPUService {
/** 缓存上次的 CPU ticks,避免每次采集都 sleep 1 秒 */
private volatile long[] prevTicks = null;
private volatile long prevTicksTimestamp = 0;
@Override
public CpuVO get() {
CpuVO cpuVO = CpuVO.builder().build();
@@ -22,76 +27,86 @@ public class CPUServiceImpl implements CPUService {
HardwareAbstractionLayer hal = si.getHardware();
CentralProcessor processor = hal.getProcessor();
OperatingSystem os = si.getOperatingSystem();
System.out.println("=========================================================");
// 1. CPU基本信息
cpuVO.setNum(processor.getPhysicalProcessorCount()); // CUP数量
System.out.println("=== CPU基本信息 ===");
System.out.println("CPU型号: " + processor.getProcessorIdentifier().getName());
System.out.println("物理核心数: " + processor.getPhysicalProcessorCount());
System.out.println("逻辑核心数: " + processor.getLogicalProcessorCount());
System.out.println("最大频率: " + processor.getMaxFreq() / 1_000_000.0 + " GHz");
cpuVO.setNum(processor.getPhysicalProcessorCount());
AssertLog.info("CPU型号: {}, 物理核心数: {}, 逻辑核心数: {}, 最大频率: {} GHz",
processor.getProcessorIdentifier().getName(),
processor.getPhysicalProcessorCount(),
processor.getLogicalProcessorCount(),
processor.getMaxFreq() / 1_000_000.0);
// 2. CPU负载信息
System.out.println("\n=== CPU负载信息 ===");
double[] loadAverage = processor.getSystemLoadAverage(3);
cpuVO.setAvg1(loadAverage[0]);// CUP1分钟负载
cpuVO.setAvg1(loadAverage[1]);// CUP5分钟负载
cpuVO.setAvg1(loadAverage[2]);// CUP15分钟负载
System.out.println("1分钟平均负载: " + loadAverage[0]);
System.out.println("5分钟平均负载: " + loadAverage[1]);
System.out.println("15分钟平均负载: " + loadAverage[2]);
// 3. CPU使用率(需要两次采样)
System.out.println("\n=== CPU使用率 ===");
long[] prevTicks = processor.getSystemCpuLoadTicks();
TimeUnit.SECONDS.sleep(1); // 等待1秒
// 计算使用率
double cpuUsage = processor.getSystemCpuLoadBetweenTicks(prevTicks);
cpuVO.setUti(cpuUsage * 100); // CPU使用率
System.out.printf("CPU 使用率: %.2f%%\n", cpuUsage * 100);
long[] ticks = processor.getSystemCpuLoadTicks();
long user = ticks[CentralProcessor.TickType.USER.getIndex()] -
cpuVO.setAvg1(loadAverage[0]); // CPU 1分钟负载
cpuVO.setAvg5(loadAverage[1]); // CPU 5分钟负载
cpuVO.setAvg15(loadAverage[2]); // CPU 15分钟负载
// 3. CPU使用率 - 使用缓存的 ticks 避免每次 sleep 1 秒
long[] currentTicks = processor.getSystemCpuLoadTicks();
long currentTime = System.currentTimeMillis();
double cpuUsage = 0;
if (prevTicks != null && currentTime - prevTicksTimestamp >= 500) {
// 使用上次缓存的 ticks 计算使用率
cpuUsage = processor.getSystemCpuLoadBetweenTicks(prevTicks);
} else {
// 首次采集或间隔太短,需要等待
prevTicks = processor.getSystemCpuLoadTicks();
prevTicksTimestamp = currentTime;
TimeUnit.MILLISECONDS.sleep(500);
currentTicks = processor.getSystemCpuLoadTicks();
cpuUsage = processor.getSystemCpuLoadBetweenTicks(prevTicks);
}
cpuVO.setUti(cpuUsage * 100);
long user = currentTicks[CentralProcessor.TickType.USER.getIndex()] -
prevTicks[CentralProcessor.TickType.USER.getIndex()];
long nice = ticks[CentralProcessor.TickType.NICE.getIndex()] -
long nice = currentTicks[CentralProcessor.TickType.NICE.getIndex()] -
prevTicks[CentralProcessor.TickType.NICE.getIndex()];
long sys = ticks[CentralProcessor.TickType.SYSTEM.getIndex()] -
long sys = currentTicks[CentralProcessor.TickType.SYSTEM.getIndex()] -
prevTicks[CentralProcessor.TickType.SYSTEM.getIndex()];
long idle = ticks[CentralProcessor.TickType.IDLE.getIndex()] -
long idle = currentTicks[CentralProcessor.TickType.IDLE.getIndex()] -
prevTicks[CentralProcessor.TickType.IDLE.getIndex()];
long iowait = ticks[CentralProcessor.TickType.IOWAIT.getIndex()] -
long iowait = currentTicks[CentralProcessor.TickType.IOWAIT.getIndex()] -
prevTicks[CentralProcessor.TickType.IOWAIT.getIndex()];
long irq = ticks[CentralProcessor.TickType.IRQ.getIndex()] -
long irq = currentTicks[CentralProcessor.TickType.IRQ.getIndex()] -
prevTicks[CentralProcessor.TickType.IRQ.getIndex()];
long softirq = ticks[CentralProcessor.TickType.SOFTIRQ.getIndex()] -
long softirq = currentTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()] -
prevTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()];
long steal = ticks[CentralProcessor.TickType.STEAL.getIndex()] -
long steal = currentTicks[CentralProcessor.TickType.STEAL.getIndex()] -
prevTicks[CentralProcessor.TickType.STEAL.getIndex()];
long total = user + nice + sys + idle + iowait + irq + softirq + steal;
// 4. CPU时间累计值
System.out.println("\n=== CPU时间累计值 ===");
long[] allTicks = processor.getSystemCpuLoadTicks();
System.out.println("中断累计时间: " +
(allTicks[CentralProcessor.TickType.IRQ.getIndex()] +
allTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()]));
System.out.println("空闲累计时间: " + allTicks[CentralProcessor.TickType.IDLE.getIndex()]);
System.out.printf("I/O等待时间(CPU等待响应时间): %.2f%%\n", 100d * iowait / total);
System.out.println("系统累计时间: " + allTicks[CentralProcessor.TickType.SYSTEM.getIndex()]);
long softwareTime = sys - irq - softirq;
System.out.printf("软件相关时间(近似无响应时间): %.2f%%\n", 100d * softwareTime / total);
System.out.println("用户进程累计时间: " + allTicks[CentralProcessor.TickType.USER.getIndex()]);
if (total > 0) {
cpuVO.setIowait(100d * iowait / total);
long softwareTime = sys - irq - softirq;
cpuVO.setNoresp(100d * softwareTime / total);
} else {
cpuVO.setIowait(0d);
cpuVO.setNoresp(0d);
}
cpuVO.setInterrupt((allTicks[CentralProcessor.TickType.IRQ.getIndex()] +
allTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()])); // CPU硬件中断提供服务时间
cpuVO.setIdle(allTicks[CentralProcessor.TickType.IDLE.getIndex()]);// CPU空闲时间
cpuVO.setIowait(100d * iowait / total);// CPU等待响应时间
cpuVO.setSystem(allTicks[CentralProcessor.TickType.SYSTEM.getIndex()]);// CPU系统时间
cpuVO.setNoresp(100d * softwareTime / total);// CPU软件无响应时间
cpuVO.setUser(allTicks[CentralProcessor.TickType.USER.getIndex()]);// CPU用户进程所花费的时间
cpuVO.setInterrupt(currentTicks[CentralProcessor.TickType.IRQ.getIndex()] +
currentTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()]);
cpuVO.setIdle(currentTicks[CentralProcessor.TickType.IDLE.getIndex()]);
cpuVO.setSystem(currentTicks[CentralProcessor.TickType.SYSTEM.getIndex()]);
cpuVO.setUser(currentTicks[CentralProcessor.TickType.USER.getIndex()]);
// 5. 系统运行时间和CPU空闲时间
// 更新缓存
prevTicks = currentTicks;
prevTicksTimestamp = currentTime;
// 5. 系统运行时间
long uptime = os.getSystemUptime();
cpuVO.setNormal(uptime);// CPU正常运行时间
System.out.println("CPU正常运行时间: " + cpuVO.getNormal());
cpuVO.setNormal(uptime);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
AssertLog.error("CPU采集被中断", e);
} catch (Exception e) {
AssertLog.error("CPU采集异常", e);
}
return cpuVO;
}
@@ -3,6 +3,7 @@ package com.tongran.agent.client.service.impl;
import com.tongran.agent.client.core.vo.DiskVO;
import com.tongran.agent.client.core.vo.PointVO;
import com.tongran.agent.client.service.DiskService;
import com.tongran.agent.client.utils.AssertLog;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import oshi.SystemInfo;
@@ -20,29 +21,24 @@ public class DiskServiceImpl implements DiskService {
List<DiskVO> tempList = new ArrayList<>();
List<DiskVO> resultList = new ArrayList<>();
SystemInfo si = new SystemInfo();
System.out.println("=========================================================");
// 获取磁盘IO信息
System.out.println("\n=== 磁盘IO信息 ===");
for (HWDiskStore disk : si.getHardware().getDiskStores()) {
DiskVO diskVO = DiskVO.builder().timestamp(timestamp).build();
diskVO.setName(disk.getName());//磁盘名称
diskVO.setSerial(disk.getSerial());//序列号
diskVO.setTotal(disk.getSize());//磁盘大小
diskVO.setWriteTimes(disk.getWrites());//磁盘写入次数
diskVO.setReadTimes(disk.getReads());//磁盘读取次数
diskVO.setWriteBytes(disk.getReadBytes());//磁盘写入字节
diskVO.setReadBytes(disk.getWriteBytes());//磁盘读取字节
diskVO.setName(disk.getName());
diskVO.setSerial(disk.getSerial());
diskVO.setTotal(disk.getSize());
diskVO.setWriteTimes(disk.getWrites());
diskVO.setReadTimes(disk.getReads());
diskVO.setWriteBytes(disk.getWriteBytes()); // 修复:写入字节
diskVO.setReadBytes(disk.getReadBytes()); // 修复:读取字节
tempList.add(diskVO);
System.out.println("磁盘名称: " + diskVO.getName());
System.out.println("序列号: " + diskVO.getSerial());
System.out.println("磁盘大小: " + diskVO.getTotal());
System.out.println("磁盘写入次数: " + diskVO.getWriteTimes());
System.out.println("磁盘读取次数: " + diskVO.getReadTimes());
System.out.println("磁盘写入字节: " + diskVO.getWriteBytes());
System.out.println("磁盘读取字节: " + diskVO.getReadBytes());
AssertLog.info("磁盘: {}, 序列号: {}, 大小: {}, 写入字节: {}, 读取字节: {}",
diskVO.getName(), diskVO.getSerial(), diskVO.getTotal(),
diskVO.getWriteBytes(), diskVO.getReadBytes());
}
try{
try {
// 第一次采样
List<HWDiskStore> disks1 = si.getHardware().getDiskStores();
long[] readBytes1 = new long[disks1.size()];
@@ -59,18 +55,20 @@ public class DiskServiceImpl implements DiskService {
long readDiff = disks2.get(i).getReadBytes() - readBytes1[i];
long writeDiff = disks2.get(i).getWriteBytes() - writeBytes1[i];
String serial = disks2.get(i).getSerial();
DiskVO diskVO = tempList.stream().filter(d -> StringUtils.equals(d.getSerial(),serial)).findFirst().orElse(null);
if(Objects.nonNull(diskVO)){
diskVO.setWriteSpeed(readDiff);//磁盘写入速率
diskVO.setReadSpeed(writeDiff);//磁盘读取速率
System.out.println("磁盘名称: " + diskVO.getName());
System.out.println("磁盘写入速率: " + diskVO.getWriteSpeed());
System.out.println("磁盘读取速率: " + diskVO.getReadSpeed());
DiskVO diskVO = tempList.stream().filter(d -> StringUtils.equals(d.getSerial(), serial)).findFirst().orElse(null);
if (Objects.nonNull(diskVO)) {
diskVO.setWriteSpeed(writeDiff); // 修复:写入速率 = 写入差值
diskVO.setReadSpeed(readDiff); // 修复:读取速率 = 读取差值
AssertLog.info("磁盘: {}, 写入速率: {}, 读取速率: {}",
diskVO.getName(), diskVO.getWriteSpeed(), diskVO.getReadSpeed());
}
resultList.add(diskVO);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
AssertLog.error("磁盘采集被中断", e);
} catch (Exception e) {
e.printStackTrace();
AssertLog.error("磁盘速率采集异常", e);
}
return resultList;
}
@@ -80,8 +78,6 @@ public class DiskServiceImpl implements DiskService {
List<PointVO> list = new ArrayList<>();
SystemInfo si = new SystemInfo();
// 获取文件系统信息
System.out.println("=========================================================");
System.out.println("=== 挂载信息 ===");
for (OSFileStore fs : si.getOperatingSystem().getFileSystem().getFileStores()) {
long totalSpace = fs.getTotalSpace();
long usableSpace = fs.getUsableSpace();
@@ -90,18 +86,15 @@ public class DiskServiceImpl implements DiskService {
(double) (totalSpace - freeSpace) / totalSpace * 100 : 0;
PointVO pointVO = PointVO.builder().timestamp(timestamp).build();
pointVO.setMount(fs.getMount());//挂载点
pointVO.setVfsType(fs.getType());//文件系统类型
pointVO.setVfsTotal(totalSpace);//总空间
pointVO.setVfsFree(usableSpace);//可用空间
pointVO.setVfsUtil(usagePercentage);//空间利用率
pointVO.setMount(fs.getMount());
pointVO.setVfsType(fs.getType());
pointVO.setVfsTotal(totalSpace);
pointVO.setVfsFree(usableSpace);
pointVO.setVfsUtil(usagePercentage);
list.add(pointVO);
System.out.println("挂载点: " + pointVO.getMount());
System.out.println("文件系统类型: " + pointVO.getVfsType());
System.out.println("总空间: " + pointVO.getVfsTotal());
System.out.println("可用空间: " + pointVO.getVfsFree());
System.out.printf("空间利用率: %.2f%%\n", usagePercentage);
AssertLog.info("挂载点: {}, 类型: {}, 总空间: {}, 可用: {}, 利用率: {}%",
pointVO.getMount(), pointVO.getVfsType(), pointVO.getVfsTotal(),
pointVO.getVfsFree(), String.format("%.2f", usagePercentage));
}
return list;
}
@@ -8,6 +8,7 @@ import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
import com.github.dockerjava.transport.DockerHttpClient;
import com.tongran.agent.client.core.vo.DockerVO;
import com.tongran.agent.client.service.DockerService;
import com.tongran.agent.client.utils.AssertLog;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
@@ -17,44 +18,32 @@ import java.io.InputStreamReader;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Service
public class DockerServiceImpl implements DockerService {
/** 复用 DockerClient 配置,避免每次创建 */
private volatile DockerClient dockerClientCache;
private volatile DockerHttpClient httpClientCache;
@Override
public List<DockerVO> dockerList(long timestamp) {
List<DockerVO> list = new ArrayList<>();
// 配置Docker客户端
DefaultDockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder()
.withDockerHost("unix:///var/run/docker.sock")
.withDockerTlsVerify(false) // 根据你的配置调整
.build();
DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
.dockerHost(config.getDockerHost())
.sslConfig(config.getSSLConfig())
.maxConnections(100)
.connectionTimeout(Duration.ofSeconds(30))
.responseTimeout(Duration.ofSeconds(45))
.build();
DockerClient dockerClient = DockerClientImpl.getInstance(config, httpClient);
DockerClient dockerClient = getDockerClient();
try {
// 获取正在运行的容器列表
List<Container> containers = dockerClient.listContainersCmd()
.withShowAll(false) // 只显示运行中的容器
.withShowAll(false)
.exec();
// 打印容器信息
System.out.println("运行中的Docker容器:");
System.out.println("容器ID\t\t镜像\t\t状态\t\t名称");
for (Container container : containers) {
DockerVO dockerVO = DockerVO.builder().timestamp(timestamp).build();
String id = container.getId().substring(0, 12); // 只显示短ID
String image = container.getImage().length() > 15 ?
String id = container.getId().substring(0, 12);
String image = container.getImage() != null && container.getImage().length() > 15 ?
container.getImage().substring(0, 15) + "..." : container.getImage();
String status = container.getStatus();
String name = container.getNames()[0].replaceFirst("/", "");
System.out.printf("%s\t%s\t%s\t%s%n", id, image, status, name);
AssertLog.info("Docker容器: id={}, image={}, status={}, name={}", id, image, status, name);
dockerVO.setId(id);
dockerVO.setName(name);
dockerVO.setStatus(status);
@@ -62,74 +51,115 @@ public class DockerServiceImpl implements DockerService {
list.add(res);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
dockerClient.close();
} catch (IOException e) {
e.printStackTrace();
}
AssertLog.error("Docker容器列表获取异常", e);
}
return list;
}
/**
* 获取 Docker 客户端(复用连接)
*/
private DockerClient getDockerClient() {
if (dockerClientCache == null) {
synchronized (this) {
if (dockerClientCache == null) {
DefaultDockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder()
.withDockerHost("unix:///var/run/docker.sock")
.withDockerTlsVerify(false)
.build();
httpClientCache = new ApacheDockerHttpClient.Builder()
.dockerHost(config.getDockerHost())
.sslConfig(config.getSSLConfig())
.maxConnections(100)
.connectionTimeout(Duration.ofSeconds(30))
.responseTimeout(Duration.ofSeconds(45))
.build();
dockerClientCache = DockerClientImpl.getInstance(config, httpClientCache);
}
}
}
return dockerClientCache;
}
/**
* 获取容器资源使用统计 - 修复:try-with-resources + 进程超时 + 数组越界保护
*/
public DockerVO dockerStats(DockerVO dockerVO) {
//判定目标容器 ID
if(StringUtils.isBlank(dockerVO.getId())){
if (StringUtils.isBlank(dockerVO.getId())) {
return dockerVO;
}
Process process = null;
try {
// 执行 docker stats 命令(--no-stream 表示只输出一次)
Process process = new ProcessBuilder(
process = new ProcessBuilder(
"docker", "stats", "--no-stream", dockerVO.getId(),
"--format", "'table {{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemPerc}}\t{{.NetIO}}"
).start();
// 读取命令输出
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream())
);
String line;
while ((line = reader.readLine()) != null) {
// 解析输出(示例输出格式):
// "your_container_id 0.00% 0.000 CPU % 0 B / 0 B 0 packets / 0 packets"
// 实际输出可能因 Docker 版本不同而变化,需根据实际情况调整正则表达式
if (line.contains(dockerVO.getId())) {
String[] parts = line.trim().split("\\s+");
String cpuUtil = parts[3]; // cpu使用率
String memUtil = parts[4]; // 内存使用率
// 网络流量通常在第 4 列(接收)和第 5 列(发送),格式为 "B/s" 或 "B"
dockerVO.setCpuUtil(cpuUtil);
String rxRate = "0B";// 接收速率(如 "1.23kB/s"
String txRate = "0B";// 发送速率(如 "4.56kB/s"
//数据间有空格
if(parts.length > 9){
rxRate = parts[5]+parts[6];
txRate = parts[8]+parts[9];
}else{
rxRate = parts[5];
txRate = parts[7];
// 修复:使用 try-with-resources 管理 reader
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.contains(dockerVO.getId())) {
String[] parts = line.trim().split("\\s+");
// 修复:数组越界保护
if (parts.length < 6) {
AssertLog.warn("docker stats 输出格式异常,字段不足: {}", line);
continue;
}
String cpuUtil = parts[3];
String memUtil = parts[4];
String rxRate;
String txRate;
if (parts.length > 9) {
rxRate = parts[5] + parts[6];
txRate = parts[8] + parts[9];
} else {
rxRate = parts[5];
txRate = parts.length > 7 ? parts[7] : "0B";
}
dockerVO.setCpuUtil(cpuUtil);
dockerVO.setMemUtil(memUtil);
dockerVO.setNetInSpeed(rxRate);
dockerVO.setNetOutSpeed(txRate);
AssertLog.info("容器网络流量 - 接收: {}, 发送: {}", rxRate, txRate);
}
// 网络流量通常在第 4 列(接收)和第 5 列(发送),格式为 "B/s" 或 "B"
dockerVO.setCpuUtil(cpuUtil);
dockerVO.setMemUtil(memUtil);
dockerVO.setNetInSpeed(rxRate);
dockerVO.setNetOutSpeed(txRate);
System.out.println("==================== 容器网络流量 ====================");
System.out.println("接收速率: " + rxRate);
System.out.println("发送速率: " + txRate);
}
}
// 等待命令执行完成并获取退出码
int exitCode = process.waitFor();
if (exitCode != 0) {
System.err.println("命令执行失败,退出码: " + exitCode);
// 修复:设置超时
boolean finished = process.waitFor(10, TimeUnit.SECONDS);
if (!finished) {
process.destroyForcibly();
AssertLog.warn("docker stats 命令超时");
} else if (process.exitValue() != 0) {
AssertLog.warn("docker stats 命令执行失败,退出码: {}", process.exitValue());
}
} catch (IOException e) {
AssertLog.error("docker stats IO异常", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
AssertLog.error("docker stats 被中断", e);
} finally {
if (process != null && process.isAlive()) {
process.destroyForcibly();
}
reader.close();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
return dockerVO;
}
/**
* 应用关闭时清理资源
*/
public void cleanup() {
try {
if (dockerClientCache != null) {
dockerClientCache.close();
}
if (httpClientCache != null) {
httpClientCache.close();
}
} catch (IOException e) {
AssertLog.error("Docker客户端关闭异常", e);
}
}
}
@@ -3,6 +3,7 @@ package com.tongran.agent.client.service.impl;
import com.tongran.agent.client.core.vo.MemoryVO;
import com.tongran.agent.client.service.MemoryService;
import com.tongran.agent.client.utils.AgentDataUtil;
import com.tongran.agent.client.utils.AssertLog;
import org.springframework.stereotype.Service;
import java.io.IOException;
@@ -15,60 +16,48 @@ public class MemoryServiceImpl implements MemoryService {
MemoryVO memoryVO = MemoryVO.builder().build();
try {
Map<String, Long> memInfo = AgentDataUtil.parseMemInfo();
System.out.println("=========================================================");
// 1. 基本内存信息
System.out.println("=== 基本内存信息 (单位KB) ===");
System.out.println("总内存: " + memInfo.get("MemTotal"));
System.out.println("空闲内存: " + memInfo.get("MemFree"));
System.out.println("可用内存: " +
memInfo.getOrDefault("MemAvailable",
memInfo.get("MemFree") +
memInfo.getOrDefault("Buffers", 0L) +
memInfo.getOrDefault("Cached", 0L) +
memInfo.getOrDefault("SReclaimable", 0L)));
memoryVO.setAvailable(memInfo.getOrDefault("MemAvailable",
memInfo.get("MemFree") +
memInfo.getOrDefault("Buffers", 0L) +
memInfo.getOrDefault("Cached", 0L) +
memInfo.getOrDefault("SReclaimable", 0L))); //可用内存
memoryVO.setTotal(memInfo.get("MemTotal")); //总内存
memoryVO.setPercent((double) memoryVO.getAvailable() / memoryVO.getTotal() * 100); //可用内存百分比
// 3. 交换空间信息
System.out.println("\n=== 交换空间信息 ===");
long swapTotal = memInfo.getOrDefault("SwapTotal", 0L);
long swapFree = memInfo.getOrDefault("SwapFree", 0L);
System.out.println("总交换空间: " + swapTotal);
System.out.println("空闲交换空间: " + swapFree);
System.out.println("已用交换空间: " + (swapTotal - swapFree));
memoryVO.setSwapSizeFree(swapFree); //交换卷/文件的可用空间(字节)
memoryVO.setSwapSizePercent((double) swapFree / swapTotal * 100); //可用交换空间百分比
if (swapTotal > 0) {
System.out.printf("交换空间使用率: %.2f%%\n",
(double)(swapTotal - swapFree) / swapTotal * 100);
}
// 4. 内存使用率分析
System.out.println("\n=== 内存使用率分析 ===");
long total = memInfo.get("MemTotal");
long available = memInfo.getOrDefault("MemAvailable",
memInfo.get("MemFree") +
long memTotal = memInfo.getOrDefault("MemTotal", 0L);
long memAvailable = memInfo.getOrDefault("MemAvailable",
memInfo.getOrDefault("MemFree", 0L) +
memInfo.getOrDefault("Buffers", 0L) +
memInfo.getOrDefault("Cached", 0L) +
memInfo.getOrDefault("SReclaimable", 0L));
// 总内存使用率
double totalUsage = (double)(total - available) / total * 100;
System.out.printf("总内存使用率: %.2f%%\n", totalUsage);
// 实际内存使用率
memoryVO.setAvailable(memAvailable);
memoryVO.setTotal(memTotal);
// 修复:除零保护
memoryVO.setPercent(memTotal > 0 ? (double) memAvailable / memTotal * 100 : 0);
AssertLog.info("总内存: {} KB, 可用内存: {} KB, 可用百分比: {}%",
memTotal, memAvailable, String.format("%.2f", memoryVO.getPercent()));
// 2. 交换空间信息
long swapTotal = memInfo.getOrDefault("SwapTotal", 0L);
long swapFree = memInfo.getOrDefault("SwapFree", 0L);
memoryVO.setSwapSizeFree(swapFree);
// 修复:除零保护
memoryVO.setSwapSizePercent(swapTotal > 0 ? (double) swapFree / swapTotal * 100 : 0);
if (swapTotal > 0) {
AssertLog.info("交换空间 - 总计: {} KB, 空闲: {} KB, 已用: {} KB, 使用率: {}%",
swapTotal, swapFree, swapTotal - swapFree,
String.format("%.2f", (double) (swapTotal - swapFree) / swapTotal * 100));
}
// 3. 内存使用率分析
long cached = memInfo.getOrDefault("Cached", 0L) +
memInfo.getOrDefault("SReclaimable", 0L);
long buffers = memInfo.getOrDefault("Buffers", 0L);
double actualUsage = (double)(total - available - cached - buffers) / total * 100;
System.out.printf("实际内存使用率: %.2f%%\n", actualUsage);
memoryVO.setUntilzation(actualUsage); //内存利用率
double actualUsage = memTotal > 0 ?
(double) (memTotal - memAvailable - cached - buffers) / memTotal * 100 : 0;
memoryVO.setUntilzation(actualUsage);
AssertLog.info("内存利用率: {}%", String.format("%.2f", actualUsage));
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("内存信息采集异常", e);
}
return memoryVO;
}
}
@@ -0,0 +1,370 @@
package com.tongran.agent.client.service.impl;
import com.tongran.agent.client.core.config.ApplicationProperties;
import com.tongran.agent.client.core.vo.NetBusinessVO;
import com.tongran.agent.client.service.NetBusinessService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.io.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Service
public class NetBusinessServiceImpl implements NetBusinessService {
private static final String CGROUP_BASE = "/sys/fs/cgroup/net_cls";
private static final String CGROUP_PREFIX = "agent_";
private final ConcurrentHashMap<String, Long> processClassIdMap = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, String> processCgroupPathMap = new ConcurrentHashMap<>();
private final Set<String> activeProcesses = ConcurrentHashMap.newKeySet();
private List<String> processList = new ArrayList<>();
@Resource
private ApplicationProperties properties;
@PostConstruct
public void init() {
try {
this.loadProcessListFromFile();
for (String processName : this.processList) {
List<Integer> pids = this.getProcessPids(processName);
if (!pids.isEmpty()) {
this.createProcessCgroup(processName);
this.initProcessChains("iptables", processName);
this.initProcessChains("ip6tables", processName);
this.addProcessToCgroup(processName, pids);
this.activeProcesses.add(processName);
log.info("进程 {} (PID: {}) 初始化完成", processName, pids);
} else {
log.info("进程 {} 当前无PID运行,跳过初始化", processName);
}
}
log.info("=== cgroup多进程监控初始化完成, 监控进程列表: {} ===", this.processList);
} catch (Exception e) {
log.error("初始化失败: {}", e.getMessage(), e);
}
}
@PreDestroy
public void cleanup() {
try {
for (String processName : this.activeProcesses) {
this.cleanupProcessRules("iptables", processName);
this.cleanupProcessRules("ip6tables", processName);
this.removeProcessCgroup(processName);
}
log.info("已清理所有 iptables 规则和 cgroup");
} catch (Exception e) {
log.error("清理失败: {}", e.getMessage(), e);
}
}
@Override
public List<NetBusinessVO> netList(long timestamp) {
List<NetBusinessVO> resultList = new ArrayList<>();
try {
List<String> oldProcessList = new ArrayList<>(this.processList);
this.loadProcessListFromFile();
Set<String> removedProcesses = new HashSet<>(oldProcessList);
removedProcesses.removeAll(this.processList);
if (!removedProcesses.isEmpty()) {
log.info("检测到配置文件中已删除的进程: {},开始清理...", removedProcesses);
for (String processName : removedProcesses) {
this.cleanupProcessRules("iptables", processName);
this.cleanupProcessRules("ip6tables", processName);
this.removeProcessCgroup(processName);
this.activeProcesses.remove(processName);
log.info("已清理进程 {} 的所有规则", processName);
}
}
Set<String> addedProcesses = new HashSet<>(this.processList);
addedProcesses.removeAll(oldProcessList);
if (!addedProcesses.isEmpty()) {
log.info("检测到新增的进程: {}", addedProcesses);
for (String processName : addedProcesses) {
List<Integer> pids = this.getProcessPids(processName);
if (!pids.isEmpty()) {
this.createProcessCgroup(processName);
this.initProcessChains("iptables", processName);
this.initProcessChains("ip6tables", processName);
this.addProcessToCgroup(processName, pids);
this.activeProcesses.add(processName);
log.info("已为进程 {} (PID: {}) 创建规则", processName, pids);
} else {
log.info("进程 {} 当前无PID,暂不创建规则", processName);
}
}
}
for (String processName : this.processList) {
List<Integer> pids = this.getProcessPids(processName);
if (!pids.isEmpty()) {
if (!this.activeProcesses.contains(processName)) {
log.info("进程 {} 已有PID运行但无规则,开始创建规则...", processName);
this.createProcessCgroup(processName);
this.initProcessChains("iptables", processName);
this.initProcessChains("ip6tables", processName);
this.activeProcesses.add(processName);
}
this.addProcessToCgroup(processName, pids);
this.ensureProcessChainsExist("iptables", processName);
this.ensureProcessChainsExist("ip6tables", processName);
long[] v4Stats = this.getTrafficStatsForProcess("iptables", processName);
long[] v6Stats = this.getTrafficStatsForProcess("ip6tables", processName);
NetBusinessVO vo = new NetBusinessVO();
vo.setIpv4InSpeed(v4Stats[0]);
vo.setIpv4OutSpeed(v4Stats[1]);
vo.setIpv6InSpeed(v6Stats[0]);
vo.setIpv6OutSpeed(v6Stats[1]);
vo.setInSpeed(v4Stats[0] + v6Stats[0]);
vo.setOutSpeed(v4Stats[1] + v6Stats[1]);
vo.setTimestamp(timestamp);
vo.setName("total");
vo.setProcessName(processName);
resultList.add(vo);
if (v4Stats[0] > 0 || v4Stats[1] > 0 || v6Stats[0] > 0 || v6Stats[1] > 0) {
log.debug("进程 {} (PID: {}): IPv4收={} 发={}, IPv6收={} 发={}", processName, pids, v4Stats[0], v4Stats[1], v6Stats[0], v6Stats[1]);
}
} else {
log.debug("进程 {} 当前无PID运行,保留已有规则等待进程启动", processName);
}
}
} catch (Exception e) {
log.error("获取网络统计失败: {}", e.getMessage(), e);
}
return resultList;
}
private long generateClassId(String processName) {
int hash = Math.abs(processName.hashCode()) & 0xFFFFF;
if (hash == 0) hash = 1;
return (long) hash & 0xFFFFFFFFL;
}
private void createProcessCgroup(String processName) {
try {
long classId = this.generateClassId(processName);
String cgroupPath = CGROUP_BASE + "/" + CGROUP_PREFIX + processName;
this.executeCommand("mkdir -p " + cgroupPath);
this.executeCommand("echo " + classId + " > " + cgroupPath + "/net_cls.classid");
this.processClassIdMap.put(processName, classId);
this.processCgroupPathMap.put(processName, cgroupPath);
log.info("进程 {} cgroup 创建完成, classid={}, path={}", processName, classId, cgroupPath);
} catch (Exception e) {
log.error("创建进程 cgroup 失败 {}: {}", processName, e.getMessage());
}
}
private void removeProcessCgroup(String processName) {
try {
String cgroupPath = this.processCgroupPathMap.get(processName);
if (cgroupPath != null) {
this.executeCommand("cat " + cgroupPath + "/tasks | while read pid; do echo $pid > " + CGROUP_BASE + "/tasks 2>/dev/null; done");
this.executeCommand("rmdir " + cgroupPath + " 2>/dev/null");
this.processClassIdMap.remove(processName);
this.processCgroupPathMap.remove(processName);
log.info("已删除进程 {} 的 cgroup", processName);
}
} catch (Exception e) {
log.error("删除进程 cgroup 失败 {}: {}", processName, e.getMessage());
}
}
private synchronized void loadProcessListFromFile() {
List<String> newProcessList = new ArrayList<>();
String path = properties.getConfPath() + "/process.conf";
File file = new File(path);
if (file.exists()) {
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
String line;
while ((line = br.readLine()) != null) {
if ((line = line.trim()).isEmpty() || line.startsWith("#")) continue;
newProcessList.add(line);
}
log.info("从 {} 加载进程列表: {}", path, newProcessList);
} catch (Exception e) {
log.error("读取配置文件失败 {}: {}", path, e.getMessage());
}
}
this.processList.clear();
this.processList.addAll(newProcessList);
}
private List<Integer> getProcessPids(String processName) {
List<Integer> pids = new ArrayList<>();
try {
Process p = new ProcessBuilder("sh", "-c", "pgrep -x " + processName + " 2>/dev/null").start();
try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) {
if (!line.trim().isEmpty()) pids.add(Integer.parseInt(line));
}
}
} catch (Exception ignored) {
}
return pids;
}
private void addProcessToCgroup(String processName, List<Integer> pids) {
String cgroupPath = this.processCgroupPathMap.get(processName);
if (cgroupPath == null) return;
for (Integer pid : pids) {
try {
Process check = new ProcessBuilder("sh", "-c", "cat " + cgroupPath + "/tasks 2>/dev/null | grep -q " + pid).start();
if (check.waitFor() == 0) continue;
this.executeCommand("echo " + pid + " > " + cgroupPath + "/tasks");
log.debug("进程 {} ({}) 已加入 cgroup", pid, processName);
} catch (Exception ignored) {
}
}
}
private String generateSafeChainName(String processName) {
String safe = processName.replaceAll("[^a-zA-Z0-9]", "_");
String hash = Integer.toHexString(processName.hashCode());
while (hash.length() < 8) hash = "0" + hash;
if (hash.length() > 8) hash = hash.substring(0, 8);
String prefix = safe.length() > 12 ? safe.substring(0, 12) : safe;
return prefix + "_" + hash;
}
private String buildChainName(String safeName, String suffix, String direction) {
String baseName = "am_" + safeName;
int maxNeeded = Math.max(baseName.length() + 6, baseName.length() + 7);
if (maxNeeded > 28) {
int baseMaxLen = 21;
if (baseName.length() > baseMaxLen) {
String prefix = baseName.substring(0, 8);
String suffixPart = baseName.substring(baseName.length() - (baseMaxLen - 9));
baseName = prefix + "_" + suffixPart;
}
}
return baseName + "_" + suffix + "_" + direction;
}
private long getProcessClassId(String processName) {
Long classId = this.processClassIdMap.get(processName);
if (classId == null) {
classId = this.generateClassId(processName);
this.processClassIdMap.put(processName, classId);
}
return classId;
}
private void initProcessChains(String command, String processName) {
long classId = this.getProcessClassId(processName);
String safeName = this.generateSafeChainName(processName);
String suffix = command.equals("iptables") ? "v4" : "v6";
String inChain = this.buildChainName(safeName, suffix, "in");
String outChain = this.buildChainName(safeName, suffix, "out");
this.initChain(command, inChain, "INPUT", classId);
this.initChain(command, outChain, "OUTPUT", classId);
}
private void ensureProcessChainsExist(String command, String processName) {
long classId = this.getProcessClassId(processName);
String safeName = this.generateSafeChainName(processName);
String suffix = command.equals("iptables") ? "v4" : "v6";
String inChain = this.buildChainName(safeName, suffix, "in");
String outChain = this.buildChainName(safeName, suffix, "out");
this.ensureChainExists(command, inChain, "INPUT", classId);
this.ensureChainExists(command, outChain, "OUTPUT", classId);
}
private void ensureChainExists(String command, String chainName, String hookChain, long classId) {
try {
Process check = new ProcessBuilder("sh", "-c", command + " -L " + chainName + " -n >/dev/null 2>&1").start();
if (check.waitFor() != 0) {
this.executeCommand(command + " -N " + chainName);
this.executeCommand(command + " -A " + chainName + " -m cgroup --cgroup " + classId + " -j RETURN");
this.executeCommand(command + " -A " + chainName + " -j RETURN");
this.executeCommand(command + " -I " + hookChain + " 1 -m cgroup --cgroup " + classId + " -j " + chainName);
log.info("{} 链 {} 初始化完成 (classid={})", command, chainName, classId);
}
} catch (Exception e) {
log.error("确保链存在失败 {}: {}", chainName, e.getMessage());
}
}
private void initChain(String command, String chainName, String hookChain, long classId) {
try {
this.executeCommand(command + " -N " + chainName + " 2>/dev/null");
this.executeCommand(command + " -F " + chainName);
this.executeCommand(command + " -A " + chainName + " -m cgroup --cgroup " + classId + " -j RETURN");
this.executeCommand(command + " -A " + chainName + " -j RETURN");
Process check = new ProcessBuilder("sh", "-c", command + " -C " + hookChain + " -m cgroup --cgroup " + classId + " -j " + chainName + " 2>/dev/null").start();
if (check.waitFor() != 0) {
this.executeCommand(command + " -I " + hookChain + " 1 -m cgroup --cgroup " + classId + " -j " + chainName);
log.debug("{} 插入规则到 {} 成功", command, hookChain);
} else {
log.debug("{} 规则已存在于 {},跳过插入", command, hookChain);
}
log.debug("{} 链 {} 初始化完成 (classid={})", command, chainName, classId);
} catch (Exception e) {
log.error("初始化链 {} 失败: {}", chainName, e.getMessage());
}
}
private long[] getTrafficStatsForProcess(String command, String processName) {
long classId = this.getProcessClassId(processName);
String safeName = this.generateSafeChainName(processName);
String suffix = command.equals("iptables") ? "v4" : "v6";
String inChain = this.buildChainName(safeName, suffix, "in");
String outChain = this.buildChainName(safeName, suffix, "out");
return new long[]{this.getChainBytes(command, inChain), this.getChainBytes(command, outChain)};
}
private void cleanupProcessRules(String command, String processName) {
try {
Long classId = this.processClassIdMap.get(processName);
if (classId == null) return;
String safeName = this.generateSafeChainName(processName);
String suffix = command.equals("iptables") ? "v4" : "v6";
String inChain = this.buildChainName(safeName, suffix, "in");
String outChain = this.buildChainName(safeName, suffix, "out");
this.executeCommand(command + " -D INPUT -m cgroup --cgroup " + classId + " -j " + inChain + " 2>/dev/null");
this.executeCommand(command + " -D OUTPUT -m cgroup --cgroup " + classId + " -j " + outChain + " 2>/dev/null");
this.executeCommand(command + " -F " + inChain + " 2>/dev/null");
this.executeCommand(command + " -F " + outChain + " 2>/dev/null");
this.executeCommand(command + " -X " + inChain + " 2>/dev/null");
this.executeCommand(command + " -X " + outChain + " 2>/dev/null");
log.info("已清理 {} 中进程 {} 的规则", command, processName);
} catch (Exception e) {
log.error("清理进程规则失败 {}: {}", processName, e.getMessage());
}
}
private long getChainBytes(String command, String chainName) {
try {
Process p = new ProcessBuilder("sh", "-c", command + " -L " + chainName + " -v -n -x | grep RETURN | head -1").start();
try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
String line = br.readLine();
if (line == null || line.isEmpty()) return 0L;
String[] parts = line.trim().split("\\s+");
if (parts.length < 2) return 0L;
return Long.parseLong(parts[1]);
}
} catch (Exception e) {
log.error("获取链 {} 统计失败: {}", chainName, e.getMessage());
}
return 0L;
}
private void executeCommand(String command) throws Exception {
Process p = new ProcessBuilder("sh", "-c", command).start();
int exitCode = p.waitFor();
if (exitCode != 0) {
try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getErrorStream()))) {
String error = br.lines().reduce("", (a, b) -> a + "\n" + b);
if (!error.isEmpty() && !error.contains("No such file") && !error.contains("File exists")) {
log.warn("命令执行警告: {} -> {}", command, error);
}
}
}
}
}
@@ -3,11 +3,11 @@ package com.tongran.agent.client.service.impl;
import com.tongran.agent.client.core.vo.NetVO;
import com.tongran.agent.client.service.NetService;
import com.tongran.agent.client.utils.AgentUtil;
import com.tongran.agent.client.utils.AssertLog;
import org.springframework.stereotype.Service;
import oshi.SystemInfo;
import oshi.hardware.HardwareAbstractionLayer;
import oshi.hardware.NetworkIF;
import oshi.util.FormatUtil;
import java.io.BufferedReader;
import java.io.InputStreamReader;
@@ -22,25 +22,20 @@ public class NetServiceImpl implements NetService {
try {
SystemInfo si = new SystemInfo();
HardwareAbstractionLayer hal = si.getHardware();
// 获取所有网络接口
List<NetworkIF> networkIFs = hal.getNetworkIFs();
System.out.println("=========================================================");
System.out.println("===== 网卡流量统计 =====");
List<NetVO> temp = new ArrayList<>();
for (NetworkIF net : networkIFs) {
System.out.println("接口名称: " + net.getName()+"("+net.getDisplayName()+")");
System.out.println("MAC地址: " + net.getMacaddr());
System.out.print("运行状态: ");
System.out.println(net.isConnectorPresent() ? "已连接" : "未连接");
System.out.println("接口类型: " + AgentUtil.getInterfaceType(net));
System.out.println("IPv4地址: " + String.join(", ", net.getIPv4addr()));
AssertLog.info("接口: {} ({}), MAC: {}, 状态: {}",
net.getName(), net.getDisplayName(), net.getMacaddr(),
net.isConnectorPresent() ? "已连接" : "未连接");
NetVO netVO = NetVO.builder().build();
netVO.setName(net.getName()+"("+net.getDisplayName()+")");//网卡名称
netVO.setMac(net.getMacaddr());//MAC
netVO.setStatus(net.isConnectorPresent() ? "已连接" : "未连接");//运行状态
netVO.setType(AgentUtil.getInterfaceType(net));//接口类型
netVO.setIpV4(String.join(", ", net.getIPv4addr()));//IPv4
netVO.setName(net.getName() + "(" + net.getDisplayName() + ")");
netVO.setMac(net.getMacaddr());
netVO.setStatus(net.isConnectorPresent() ? "已连接" : "未连接");
netVO.setType(AgentUtil.getInterfaceType(net));
netVO.setIpV4(String.join(", ", net.getIPv4addr()));
Map<String, String> map = getNetworkMode(net.getName());
if (map != null && !map.isEmpty()) {
netVO.setSpeed(map.get("speed"));
@@ -48,75 +43,89 @@ public class NetServiceImpl implements NetService {
}
temp.add(netVO);
}
// 2. 实时带宽监控(需要两次采样)
System.out.println("\n=== 实时带宽监控 ===");
// 第一次采样
// 实时带宽监控(需要两次采样)
for (NetworkIF net : networkIFs) {
net.updateAttributes();
}
// 等待1秒
TimeUnit.SECONDS.sleep(1);
// 第二次采样并计算速率
for (NetworkIF net : networkIFs) {
long prevBytesRecv = net.getBytesRecv();
long prevBytesSent = net.getBytesSent();
net.updateAttributes();
long bytesRecv = net.getBytesRecv() - prevBytesRecv;
long bytesSent = net.getBytesSent() - prevBytesSent;
System.out.println("接口: " + net.getName());
System.out.println("入站丢包: " + net.getInDrops());
System.out.println("出站丢包: " + net.getCollisions());
System.out.println("接收带宽: " + FormatUtil.formatBytes(bytesRecv) + "/s (" +
bytesToMbps(bytesRecv) + " Mbps)");
System.out.println("发送带宽: " + FormatUtil.formatBytes(bytesSent) + "/s (" +
bytesToMbps(bytesSent) + " Mbps)");
AssertLog.info("接口: {}, 入站丢包: {}, 出站丢包: {}, 接收: {} Mbps, 发送: {} Mbps",
net.getName(), net.getInDrops(), net.getCollisions(),
String.format("%.2f", bytesToMbps(bytesRecv)), String.format("%.2f", bytesToMbps(bytesSent)));
NetVO netVO = temp.stream().filter(n -> n.getIpV4().equals(String.join(", ", net.getIPv4addr()))
&& n.getMac().equals(net.getMacaddr())).findFirst().orElse(null);
if(Objects.nonNull(netVO)){
netVO.setInDropped(net.getInDrops());//入站丢包
netVO.setOutDropped(net.getCollisions());//出站丢包
netVO.setInSpeed(bytesRecv);//接收流量
netVO.setOutSpeed(bytesSent);//发送流量
if (Objects.nonNull(netVO)) {
netVO.setInDropped(net.getInDrops());
netVO.setOutDropped(net.getCollisions());
netVO.setInSpeed(bytesRecv);
netVO.setOutSpeed(bytesSent);
netVO.setTimestamp(timestamp);
list.add(netVO);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
AssertLog.error("网络采集被中断", e);
} catch (Exception e) {
e.printStackTrace();
AssertLog.error("网络采集异常", e);
}
return list;
}
private static double bytesToMbps(long bytes) {
return bytes * 8.0 / 1_000_000; // bytes to megabits
return bytes * 8.0 / 1_000_000;
}
/**
* 获取网络接口模式信息 - 修复资源泄漏,使用 try-with-resources + 进程超时
*/
public static Map<String, String> getNetworkMode(String interfaceName) {
Map<String, String> result = new HashMap<>();
ProcessBuilder pb = new ProcessBuilder("ethtool", interfaceName);
pb.redirectErrorStream(true);
Process process = null;
try {
Process process = pb.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("Speed:")) {
result.put("speed", line.split(":")[1].trim());
} else if (line.contains("Duplex:")) {
result.put("duplex", line.split(":")[1].trim());
} else if (line.contains("Auto-negotiation:")) {
result.put("auto-negotiation", line.split(":")[1].trim());
process = pb.start();
// 修复:使用 try-with-resources 管理 reader
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("Speed:")) {
String[] parts = line.split(":");
if (parts.length > 1) result.put("speed", parts[1].trim());
} else if (line.contains("Duplex:")) {
String[] parts = line.split(":");
if (parts.length > 1) result.put("duplex", parts[1].trim());
} else if (line.contains("Auto-negotiation:")) {
String[] parts = line.split(":");
if (parts.length > 1) result.put("auto-negotiation", parts[1].trim());
}
}
}
int exitCode = process.waitFor();
if (exitCode != 0) {
result.put("error", "ethtool command failed with exit code " + exitCode);
// 修复:设置超时
boolean finished = process.waitFor(5, TimeUnit.SECONDS);
if (!finished) {
process.destroyForcibly();
result.put("error", "ethtool command timed out");
} else if (process.exitValue() != 0) {
result.put("error", "ethtool command failed with exit code " + process.exitValue());
}
} catch (Exception e) {
result.put("error", e.getMessage());
} finally {
// 修复:确保进程被销毁
if (process != null && process.isAlive()) {
process.destroyForcibly();
}
}
return result;
}
@@ -23,6 +23,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@@ -378,7 +379,7 @@ public class SwitchBoardServiceImpl implements SwitchBoardService {
}
}
private static String getInterfaceInfoByType(Snmp snmp, Target target, String type, LinkedHashMap<String,String> oidParams) throws IOException {
private static String getInterfaceInfoByType(Snmp snmp, Target target, String type, Map<String,String> oidParams) throws IOException {
if(oidParams.isEmpty()){
return "";
}
@@ -5,6 +5,7 @@ import com.tongran.agent.client.core.vo.SystemVO;
import com.tongran.agent.client.service.SystemService;
import com.tongran.agent.client.utils.AgentDataUtil;
import com.tongran.agent.client.utils.AgentUtil;
import com.tongran.agent.client.utils.AssertLog;
import org.springframework.stereotype.Service;
import oshi.SystemInfo;
import oshi.hardware.CentralProcessor;
@@ -31,44 +32,33 @@ public class SystemServiceImpl implements SystemService {
CentralProcessor processor = hal.getProcessor();
OperatingSystem os = si.getOperatingSystem();
System.out.println("=========================================================");
// 1. 操作系统基本信息
System.out.println("=== 操作系统信息 ===");
systemVO.setOs(os.getFamily()); //操作系统
systemVO.setArch(os.getVersionInfo().getVersion() + " " + os.getBitness() + ""); //操作系统架构
systemVO.setOs(os.getFamily());
systemVO.setArch(os.getVersionInfo().getVersion() + " " + os.getBitness() + "");
systemVO.setUuid(AgentUtil.getMotherboardUUID());
System.out.println("操作系统: " + systemVO.getOs());
System.out.println("操作系统架构: " + systemVO.getArch());
System.out.println("UUID: " + AgentUtil.getMotherboardUUID());
AssertLog.info("操作系统: {}, 架构: {}, UUID: {}", systemVO.getOs(), systemVO.getArch(), systemVO.getUuid());
// 2. 进程信息
System.out.println("\n=== 进程信息 ===");
long maxProcesses = getMaxProcessesLinux();
int runningProcesses = getRunningProcessesLinux();
System.out.println("最大进程数: " + maxProcesses);
System.out.println("正在运行的进程数: " + runningProcesses);
systemVO.setMaxProc(maxProcesses); //最大进程数
systemVO.setRunProcNum(runningProcesses); //正在运行的进程数
systemVO.setMaxProc(maxProcesses);
systemVO.setRunProcNum(runningProcesses);
AssertLog.info("最大进程数: {}, 运行中进程数: {}", maxProcesses, runningProcesses);
// 3. 登录用户数
System.out.println("\n=== 登录用户 ===");
systemVO.setUsersNum(os.getSessions().size()); //登录用户数
System.out.println("登录用户数: " + systemVO.getUsersNum());
systemVO.setUsersNum(os.getSessions().size());
AssertLog.info("登录用户数: {}", systemVO.getUsersNum());
// 4. 磁盘信息
System.out.println("\n=== 磁盘信息 ===");
systemVO.setDiskSizeTotal(diskSpace()); //硬盘:总可用空间
systemVO.setBootTime(systemBootTime(hal.getProcessor())); //系统启动时间
systemVO.setUname(systemDescription(si)); //系统描述
systemVO.setLocalTime(localTime()); //系统本地时间
systemVO.setUpTime(systemUptime(os)); //系统正常运行时间
System.out.println("硬盘:总可用空间: " + systemVO.getDiskSizeTotal());
System.out.println("系统启动时间: " + systemVO.getBootTime());
System.out.println("系统描述: " + systemVO.getUname());
System.out.println("系统本地时间: " + systemVO.getLocalTime());
System.out.println("系统正常运行时间: " + systemVO.getUpTime());
systemVO.setDiskSizeTotal(diskSpace());
systemVO.setBootTime(systemBootTime(hal.getProcessor()));
systemVO.setUname(systemDescription(si));
systemVO.setLocalTime(localTime());
systemVO.setUpTime(systemUptime(os));
AssertLog.info("硬盘总可用空间: {}, 系统启动时间: {}, 系统正常运行时间: {}",
systemVO.getDiskSizeTotal(), systemVO.getBootTime(), systemVO.getUpTime());
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("系统信息采集异常", e);
}
return systemVO;
}
@@ -76,8 +66,8 @@ public class SystemServiceImpl implements SystemService {
@Override
public String otherSystem(String type) {
JSONObject json = new JSONObject();
json.put("type",type);
switch(type){
json.put("type", type);
switch (type) {
case "systemSwapSizeFreeCollect":
json.put("value", String.valueOf(handleSystemSwapSizeFree()));
break;
@@ -97,10 +87,10 @@ public class SystemServiceImpl implements SystemService {
json.put("value", String.valueOf(handleMemorySizeTotal()));
break;
case "systemSwOsCollect":
json.put("value",handleSystemSwOs());
json.put("value", handleSystemSwOs());
break;
case "systemSwArchCollect":
json.put("value",handleSystemSwArch());
json.put("value", handleSystemSwArch());
break;
case "kernelMaxprocCollect":
json.put("value", String.valueOf(handleKernelMaxproc()));
@@ -118,10 +108,10 @@ public class SystemServiceImpl implements SystemService {
json.put("value", String.valueOf(handleSystemBoottime()));
break;
case "systemUnameCollect":
json.put("value",handleSystemUname());
json.put("value", handleSystemUname());
break;
case "systemLocaltimeCollect":
json.put("value",handleSystemLocaltime());
json.put("value", handleSystemLocaltime());
break;
case "systemUptimeCollect":
json.put("value", String.valueOf(handleSystemUptime()));
@@ -130,224 +120,209 @@ public class SystemServiceImpl implements SystemService {
json.put("value", String.valueOf(handleProcNum()));
break;
default:
json.put("value",handleDefault());
json.put("value", handleDefault());
break;
}
return json.toString();
}
private long handleSystemSwapSizeFree(){
private long handleSystemSwapSizeFree() {
try {
Map<String, Long> memInfo = AgentDataUtil.parseMemInfo();
long swapFree = memInfo.getOrDefault("SwapFree", 0L);
System.out.println("=========================================================");
System.out.println("交换卷/文件的可用空间(字节): " + swapFree);
AssertLog.info("交换卷可用空间: {} KB", swapFree);
return swapFree;
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("获取交换卷可用空间异常", e);
}
return 0L;
}
private double handleMemoryUtilization(){
private double handleMemoryUtilization() {
try {
Map<String, Long> memInfo = AgentDataUtil.parseMemInfo();
System.out.println("=========================================================");
long total = memInfo.get("MemTotal");
long total = memInfo.getOrDefault("MemTotal", 0L);
long available = memInfo.getOrDefault("MemAvailable",
memInfo.get("MemFree") +
memInfo.getOrDefault("MemFree", 0L) +
memInfo.getOrDefault("Buffers", 0L) +
memInfo.getOrDefault("Cached", 0L) +
memInfo.getOrDefault("SReclaimable", 0L));
// 实际内存使用率
long cached = memInfo.getOrDefault("Cached", 0L) +
memInfo.getOrDefault("SReclaimable", 0L);
long buffers = memInfo.getOrDefault("Buffers", 0L);
double actualUsage = (double)(total - available - cached - buffers) / total * 100;
System.out.printf("实际内存使用率: %.2f%%\n", actualUsage);
// 修复:除零保护
double actualUsage = total > 0 ? (double) (total - available - cached - buffers) / total * 100 : 0;
AssertLog.info("实际内存使用率: {}%", String.format("%.2f", actualUsage));
return actualUsage;
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("获取内存利用率异常", e);
}
return 0;
}
private double handleSystemSwapSizePercent(){
private double handleSystemSwapSizePercent() {
try {
Map<String, Long> memInfo = AgentDataUtil.parseMemInfo();
System.out.println("=========================================================");
long swapTotal = memInfo.getOrDefault("SwapTotal", 0L);
long swapFree = memInfo.getOrDefault("SwapFree", 0L);
System.out.printf("交换空间使用率: %.2f%%\n",
(double)(swapTotal - swapFree) / swapTotal * 100);
return (double) swapFree / swapTotal * 100;
// 修复:除零保护
if (swapTotal > 0) {
AssertLog.info("交换空间使用率: {}%",
String.format("%.2f", (double) (swapTotal - swapFree) / swapTotal * 100));
return (double) swapFree / swapTotal * 100;
}
return 0;
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("获取交换空间百分比异常", e);
}
return 0;
}
private long handleMemorySizeAvailable(){
private long handleMemorySizeAvailable() {
try {
Map<String, Long> memInfo = AgentDataUtil.parseMemInfo();
System.out.println("=========================================================");
System.out.println("可用内存: " +
memInfo.getOrDefault("MemAvailable",
memInfo.get("MemFree") +
memInfo.getOrDefault("Buffers", 0L) +
memInfo.getOrDefault("Cached", 0L) +
memInfo.getOrDefault("SReclaimable", 0L)));
return memInfo.getOrDefault("MemAvailable",
memInfo.get("MemFree") +
memInfo.getOrDefault("Buffers", 0L) +
memInfo.getOrDefault("Cached", 0L) +
memInfo.getOrDefault("SReclaimable", 0L));
} catch (IOException e) {
e.printStackTrace();
}
return 0;
}
private double handleMemorySizePercent(){
try {
Map<String, Long> memInfo = AgentDataUtil.parseMemInfo();
System.out.println("=========================================================");
long available = memInfo.getOrDefault("MemAvailable",
memInfo.get("MemFree") +
memInfo.getOrDefault("MemFree", 0L) +
memInfo.getOrDefault("Buffers", 0L) +
memInfo.getOrDefault("Cached", 0L) +
memInfo.getOrDefault("SReclaimable", 0L));
long total = memInfo.get("MemTotal");
System.out.println("可用内存百分比: " + (double) available / total * 100);
return (double) available / total * 100;
AssertLog.info("可用内存: {} KB", available);
return available;
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("获取可用内存异常", e);
}
return 0;
}
private long handleMemorySizeTotal(){
private double handleMemorySizePercent() {
try {
Map<String, Long> memInfo = AgentDataUtil.parseMemInfo();
System.out.println("=========================================================");
long total = memInfo.get("MemTotal");
System.out.println("总内存: " + total);
long available = memInfo.getOrDefault("MemAvailable",
memInfo.getOrDefault("MemFree", 0L) +
memInfo.getOrDefault("Buffers", 0L) +
memInfo.getOrDefault("Cached", 0L) +
memInfo.getOrDefault("SReclaimable", 0L));
long total = memInfo.getOrDefault("MemTotal", 0L);
// 修复:除零保护
double percent = total > 0 ? (double) available / total * 100 : 0;
AssertLog.info("可用内存百分比: {}%", String.format("%.2f", percent));
return percent;
} catch (IOException e) {
AssertLog.error("获取可用内存百分比异常", e);
}
return 0;
}
private long handleMemorySizeTotal() {
try {
Map<String, Long> memInfo = AgentDataUtil.parseMemInfo();
long total = memInfo.getOrDefault("MemTotal", 0L);
AssertLog.info("总内存: {} KB", total);
return total;
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("获取总内存异常", e);
}
return 0;
}
private String handleSystemSwOs(){
private String handleSystemSwOs() {
SystemInfo si = new SystemInfo();
OperatingSystem os = si.getOperatingSystem();
System.out.println("=========================================================");
System.out.println("操作系统: " + os.getFamily());
AssertLog.info("操作系统: {}", os.getFamily());
return os.getFamily();
}
private String handleSystemSwArch(){
private String handleSystemSwArch() {
SystemInfo si = new SystemInfo();
OperatingSystem os = si.getOperatingSystem();
System.out.println("=========================================================");
String arch = os.getVersionInfo().getVersion() + " " + os.getBitness() + "";
System.out.println("操作系统架构: " + arch);
AssertLog.info("操作系统架构: {}", arch);
return arch;
}
private long handleKernelMaxproc(){
System.out.println("=========================================================");
private long handleKernelMaxproc() {
long maxProcesses = 0;
try {
maxProcesses = getMaxProcessesLinux();
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("获取最大进程数异常", e);
}
System.out.println("最大进程数: " + maxProcesses);
AssertLog.info("最大进程数: {}", maxProcesses);
return maxProcesses;
}
private long handleProcNumRun(){
System.out.println("=========================================================");
private long handleProcNumRun() {
int runningProcesses = getRunningProcessesLinux();
System.out.println("正在运行的进程数: " + runningProcesses);
AssertLog.info("正在运行的进程数: {}", runningProcesses);
return runningProcesses;
}
private int handleUsersNum(){
private int handleUsersNum() {
SystemInfo si = new SystemInfo();
OperatingSystem os = si.getOperatingSystem();
System.out.println("=========================================================");
int usersNum = os.getSessions().size();
System.out.println("登录用户数: " + usersNum);
AssertLog.info("登录用户数: {}", usersNum);
return usersNum;
}
private long handleSystemDiskSizeTotal(){
System.out.println("=========================================================");
private long handleSystemDiskSizeTotal() {
long diskSizeTotal = diskSpace();
System.out.println("硬盘总可用空间: " + diskSizeTotal);
AssertLog.info("硬盘总可用空间: {}", diskSizeTotal);
return diskSizeTotal;
}
public long handleSystemBoottime(){
public long handleSystemBoottime() {
SystemInfo si = new SystemInfo();
HardwareAbstractionLayer hal = si.getHardware();
long boottime = systemBootTime(hal.getProcessor());
System.out.println("=========================================================");
System.out.println("系统启动时间: " + boottime);
AssertLog.info("系统启动时间: {}", boottime);
return boottime;
}
private String handleSystemUname(){
private String handleSystemUname() {
SystemInfo si = new SystemInfo();
String uname = systemDescription(si);
System.out.println("=========================================================");
System.out.println("系统描述: " + uname);
AssertLog.info("系统描述: {}", uname);
return uname;
}
private String handleSystemLocaltime(){
System.out.println("=========================================================");
private String handleSystemLocaltime() {
String time = localTime();
System.out.println("系统本地时间: " + time);
AssertLog.info("系统本地时间: {}", time);
return time;
}
private long handleSystemUptime(){
private long handleSystemUptime() {
SystemInfo si = new SystemInfo();
OperatingSystem os = si.getOperatingSystem();
System.out.println("=========================================================");
long uptime = systemUptime(os);
System.out.println("系统正常运行时间: " + uptime);
AssertLog.info("系统正常运行时间: {}", uptime);
return uptime;
}
private long handleProcNum(){
private long handleProcNum() {
SystemInfo si = new SystemInfo();
OperatingSystem os = si.getOperatingSystem();
System.out.println("=========================================================");
long procNum = os.getProcessCount();
System.out.println("进程数: " + procNum);
AssertLog.info("进程数: {}", procNum);
return procNum;
}
private String handleDefault(){
private String handleDefault() {
return "";
}
// 读取 /proc/sys/kernel/pid_max 获取最大进程数
private static long getMaxProcessesLinux() throws IOException {
File pidMaxFile = new File("/proc/sys/kernel/pid_max");
if (!pidMaxFile.exists()) {
return 0;
}
try (BufferedReader reader = new BufferedReader(new FileReader(pidMaxFile))) {
String line = reader.readLine().trim();
return Long.parseLong(line);
String line = reader.readLine();
return line != null ? Long.parseLong(line.trim()) : 0;
}
}
// 统计 /proc 下的数字目录数(每个目录对应一个进程)
private static int getRunningProcessesLinux() {
File procDir = new File("/proc");
File[] files = procDir.listFiles();
@@ -362,7 +337,6 @@ public class SystemServiceImpl implements SystemService {
return count;
}
// 获取硬盘总可用空间
public long diskSpace() {
long diskSizeTotal = 0;
File[] roots = File.listRoots();
@@ -372,30 +346,23 @@ public class SystemServiceImpl implements SystemService {
return diskSizeTotal;
}
// 获取系统启动时间
public long systemBootTime(CentralProcessor processor) {
long[] systemCpuLoadTicks = processor.getSystemCpuLoadTicks();
long bootTime = ManagementFactory.getRuntimeMXBean().getStartTime();
return bootTime;
return ManagementFactory.getRuntimeMXBean().getStartTime();
}
// 获取系统描述
public String systemDescription(SystemInfo si) {
OperatingSystem os = si.getOperatingSystem();
HardwareAbstractionLayer hal = si.getHardware();
return "操作系统: " + os.toString() + ",系统版本: " + os.getVersionInfo().toString() + "" +
return "操作系统: " + os.toString() + ",系统版本: " + os.getVersionInfo().toString() +
",处理器: " + hal.getProcessor().getProcessorIdentifier().getName() +
",物理内存: " + hal.getMemory().getTotal() / (1024 * 1024 * 1024) + " GB";
}
// 获取系统本地时间
public String localTime() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
// 获取系统正常运行时间
public long systemUptime(OperatingSystem os) {
long uptimeSeconds = os.getSystemUptime();
return uptimeSeconds;
return os.getSystemUptime();
}
}
@@ -240,4 +240,46 @@ public class AgentUtil {
return swapped;
}
/**
* 将字符串数组按行写入文件
*/
public static void bufferedWriter(String filePath, String[] lines) {
try (java.io.BufferedWriter writer = new java.io.BufferedWriter(new java.io.FileWriter(filePath))) {
for (String line : lines) {
writer.write(line);
writer.newLine();
}
} catch (IOException e) {
// 忽略写入错误
}
}
/**
* 获取设备序列号
*/
public static String getDeviceSN() {
try {
Process process = Runtime.getRuntime().exec("cat /proc/cpuinfo");
java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("Serial")) {
String[] parts = line.split(":");
if (parts.length > 1) {
return parts[1].trim();
}
}
}
reader.close();
} catch (Exception e) {
// 忽略错误
}
// 回退到主机名
try {
return java.net.InetAddress.getLocalHost().getHostName();
} catch (Exception e) {
return "unknown-device";
}
}
}
@@ -0,0 +1,104 @@
package com.tongran.agent.client.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Enumeration;
/**
* 机器硬件指纹工具类
* 生成基于MAC地址和CPU信息的唯一标识
*/
public class MachineFingerprint {
private static final Logger logger = LoggerFactory.getLogger(MachineFingerprint.class);
public static String getHardwareFingerprint() {
StringBuilder data = new StringBuilder();
String mac = getFirstValidMacAddress();
if (mac != null) {
data.append(mac);
logger.info("MAC 信息={}", mac);
} else {
logger.warn("无法获取 MAC 地址");
}
String cpuInfo = getCpuInfo();
if (cpuInfo != null) {
logger.info("CPU 信息={}", cpuInfo);
data.append(cpuInfo);
} else {
logger.warn("无法获取 CPU 信息");
}
data.append(System.currentTimeMillis());
if (data.length() == 0) {
return "unknown";
}
return md5(data.toString());
}
private static String getFirstValidMacAddress() {
try {
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface ni = interfaces.nextElement();
if (ni.isLoopback() || !ni.isUp()) continue;
byte[] mac = ni.getHardwareAddress();
if (mac == null || mac.length <= 0) continue;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < mac.length; i++) {
sb.append(String.format("%02X", mac[i]));
if (i < mac.length - 1) sb.append(":");
}
return sb.toString();
}
} catch (SocketException e) {
logger.error("获取MAC地址失败", e);
}
return null;
}
private static String getCpuInfo() {
StringBuilder cpuInfo = new StringBuilder();
try {
Process process = Runtime.getRuntime().exec("cat /proc/cpuinfo");
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
boolean found = false;
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("model name")) {
cpuInfo.append(line.split(":")[1].trim()).append(";");
found = true;
}
if (line.contains("Serial")) {
cpuInfo.append("Serial=").append(line.split(":")[1].trim());
found = true;
}
}
reader.close();
return found ? cpuInfo.toString() : null;
} catch (Exception e) {
logger.error("获取CPU信息失败", e);
return null;
}
}
private static String md5(String input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(input.getBytes());
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b & 0xFF));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 算法不可用", e);
}
}
}
@@ -0,0 +1,105 @@
package com.tongran.agent.client.utils;
import com.tongran.agent.client.core.vo.NetworkInterfaceInfo;
import java.util.ArrayList;
import java.util.List;
/**
* 网络接口工具类(从1.20恢复的简化版)
* 完整的网卡信息采集依赖PublicIpFetcher等多个类,此处提供基础实现
* collectNetworkInfo用于注册消息中的网络信息上报
*/
public class NetworkInterfaceUtil {
public static boolean getmacVlanStatus(String interfaceName) {
try {
Process process = Runtime.getRuntime().exec(
new String[]{"ping", "-I", interfaceName, "-c", "1", "-W", "2", "www.baidu.com"});
return process.waitFor() == 0;
} catch (Exception e) {
AssertLog.warn("接口 {} ping异常: {}", interfaceName, e.getMessage());
return false;
}
}
/**
* 收集网络接口信息
* 返回所有活跃的以太网接口信息列表
*/
public static List<NetworkInterfaceInfo> collectNetworkInfo() {
List<NetworkInterfaceInfo> result = new ArrayList<>();
try {
// 使用 ip link show 获取网卡列表
Process process = Runtime.getRuntime().exec("ip -o link show");
java.io.BufferedReader reader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
// 格式: 1: lo: <LOOPBACK,UP,LOWER_UP> ...
String[] parts = line.trim().split(":");
if (parts.length < 2) continue;
String name = parts[1].trim();
// 跳过回环接口
if ("lo".equals(name)) continue;
// 检查是否UP
if (!line.contains("UP")) continue;
NetworkInterfaceInfo info = NetworkInterfaceInfo.builder()
.name(name)
.type("Ethernet")
.build();
// 获取MAC地址
try {
Process macProcess = Runtime.getRuntime().exec(
"cat /sys/class/net/" + name + "/address");
java.io.BufferedReader macReader = new java.io.BufferedReader(
new java.io.InputStreamReader(macProcess.getInputStream()));
String mac = macReader.readLine();
if (mac != null && !mac.trim().isEmpty()) {
info.setMac(mac.trim());
}
macReader.close();
} catch (Exception ignored) {}
// 获取IPv4
try {
Process ipProcess = Runtime.getRuntime().exec(
new String[]{"sh", "-c", "ip -4 addr show " + name + " 2>/dev/null | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1"});
java.io.BufferedReader ipReader = new java.io.BufferedReader(
new java.io.InputStreamReader(ipProcess.getInputStream()));
String ip = ipReader.readLine();
if (ip != null && !ip.trim().isEmpty() && !"127.0.0.1".equals(ip.trim())) {
info.setIpv4(ip.trim());
}
ipReader.close();
} catch (Exception ignored) {}
result.add(info);
}
reader.close();
} catch (Exception e) {
AssertLog.warn("collectNetworkInfo失败: {}", e.getMessage());
}
return result;
}
public static long getInterfaceCreateTime(String interfaceName) {
try {
Process process = Runtime.getRuntime().exec(
new String[]{"stat", "-c", "%Y", "/sys/class/net/" + interfaceName});
java.io.BufferedReader reader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
String output = reader.readLine();
reader.close();
process.waitFor();
if (output != null && !output.trim().isEmpty()) {
return Long.parseLong(output.trim());
}
} catch (Exception e) {
AssertLog.error("获取网卡 {} 创建时间失败: {}", interfaceName, e.getMessage());
}
return -1L;
}
}
+49 -9
View File
@@ -2,8 +2,8 @@ server:
port: 7010
servlet:
context-path: /tr-agent-client
# 接口文档配置
# 接口文档配置
knife4j:
enable: true
production: false # 开启屏蔽文档资源
@@ -13,10 +13,50 @@ logging:
file:
path: /usr/local/tongran/logs
tcp:
netty:
charge:
enable: true
name: AGENT-CLIENT-服务
port: 6610
readerIdleTime: 300
# Netty SaaS服务器连接配置
netty:
server:
host: 120.211.95.173
# 生产环境
port: 6620
# 测试环境
# port: 56620
client:
client-id: client-001
reconnect-interval: 5
maxReconnectAttempts: 10 # 最大重连次数
initialReconnectDelay: 1000 # 初始重连延迟(毫秒)
maxReconnectDelay: 30000 # 最大重连延迟(毫秒)
# 安全配置(P0 修复)
# 重要:生产环境务必通过环境变量注入 secret 和 token,不要明文写 yml
agent:
security:
# === 命令执行白名单 ===
allowed-scripts-dir: /opt/tongran/scripts
allowed-script-names:
- restart.sh
- update.sh
- cleanup.sh
command-timeout-seconds: 100
# === HMAC 签名 ===
# 生产环境:export AGENT_SIGN_SECRET=xxxxxx
sign-secret: ${AGENT_SIGN_SECRET:tongran-dev-secret-change-me}
sign-ttl-seconds: 300
# === 下载安全 ===
download:
allowed-protocols:
- https
allowed-hosts:
- oss.tongran.com
- files.tongran.com
max-size-bytes: 104857600 # 100MB
sha256-required: true
allowed-save-dir: /opt/tongran/downloads
# === Netty 握手认证 ===
# 生产环境:export AGENT_NETTY_TOKEN=xxxxxx
netty-auth-token: ${AGENT_NETTY_TOKEN:tongran-dev-token-change-me}
handshake-timeout-seconds: 10
+1 -1
View File
@@ -6,7 +6,7 @@ spring:
matching-strategy: ant_path_matcher
application:
name: tr-agent-client
version: 1.0
version: 1.21
web:
resources:
static-locations: classpath*:/META-INF/resources/
@@ -0,0 +1,158 @@
package com.tongran.agent.client.security;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.util.AttributeKey;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@link AuthHandshakeHandler} 单元测试
*
* <p>覆盖:正确 token 通过错误 token 拒绝格式错误拒绝未配置 token 拒绝
* 已认证后放行后续消息等
*
* <p>使用 Netty {@link EmbeddedChannel} 做内存级测试,不真实开端口
*
* @author Senior Developer
*/
@DisplayName("Netty 握手认证处理器测试")
class AuthHandshakeHandlerTest {
private SecurityProperties properties;
private AuthHandshakeHandler handler;
private static final String TOKEN = "valid-token-123";
@BeforeEach
void setUp() {
properties = new SecurityProperties();
properties.setNettyAuthToken(TOKEN);
properties.setHandshakeTimeoutSeconds(10);
handler = new AuthHandshakeHandler(properties);
}
@Test
@DisplayName("正确 token 应通过握手")
void shouldPassWithValidToken() {
EmbeddedChannel channel = new EmbeddedChannel(handler);
// 发送握手包
String handshake = AuthHandshakeHandler.HANDSHAKE_PREFIX + TOKEN
+ AuthHandshakeHandler.HANDSHAKE_SUFFIX;
channel.writeInbound(Unpooled.copiedBuffer(handshake, StandardCharsets.UTF_8));
// 验证已认证
Boolean authenticated = channel.attr(AuthHandshakeHandler.AUTHENTICATED).get();
assertTrue(authenticated, "正确 token 后应标记为已认证");
// handler 应已被移除(认证通过后从 pipeline 摘除)
assertNull(channel.pipeline().context(AuthHandshakeHandler.class),
"认证通过后 handler 应从 pipeline 移除");
channel.finishAndReleaseAll();
}
@Test
@DisplayName("错误 token 应拒绝并关闭连接")
void shouldRejectWrongToken() {
EmbeddedChannel channel = new EmbeddedChannel(handler);
String wrongHandshake = AuthHandshakeHandler.HANDSHAKE_PREFIX + "wrong-token"
+ AuthHandshakeHandler.HANDSHAKE_SUFFIX;
channel.writeInbound(Unpooled.copiedBuffer(wrongHandshake, StandardCharsets.UTF_8));
// 应该有拒绝响应写出
ByteBuf response = channel.readOutbound();
assertNotNull(response, "应返回拒绝消息");
String responseStr = response.toString(StandardCharsets.UTF_8);
assertTrue(responseStr.startsWith("auth-failed:"), "响应应是 auth-failed 开头");
// 连接应被关闭
assertFalse(channel.isActive(), "连接应被关闭");
channel.finishAndReleaseAll();
}
@Test
@DisplayName("格式错误的消息应被拒绝")
void shouldRejectMalformedMessage() {
EmbeddedChannel channel = new EmbeddedChannel(handler);
// 不带前缀
channel.writeInbound(Unpooled.copiedBuffer("just-some-data@tong-ran", StandardCharsets.UTF_8));
assertFalse(channel.isActive(), "格式错误应关闭连接");
channel.finishAndReleaseAll();
}
@Test
@DisplayName("未配置 token 应拒绝所有连接")
void shouldRejectWhenTokenNotConfigured() {
properties.setNettyAuthToken("");
AuthHandshakeHandler handlerNoToken = new AuthHandshakeHandler(properties);
EmbeddedChannel channel = new EmbeddedChannel(handlerNoToken);
String handshake = AuthHandshakeHandler.HANDSHAKE_PREFIX + TOKEN
+ AuthHandshakeHandler.HANDSHAKE_SUFFIX;
channel.writeInbound(Unpooled.copiedBuffer(handshake, StandardCharsets.UTF_8));
assertFalse(channel.isActive(), "未配置 token 时应拒绝所有连接");
channel.finishAndReleaseAll();
}
@Test
@DisplayName("非 ByteBuf 消息应被拒绝")
void shouldRejectNonByteBufMessage() {
EmbeddedChannel channel = new EmbeddedChannel(handler);
// 直接传一个 String(模拟错误用法)
channel.writeInbound("not-a-bytebuf");
assertFalse(channel.isActive(), "非 ByteBuf 消息应关闭连接");
channel.finishAndReleaseAll();
}
@Test
@DisplayName("已认证后应放行后续消息")
void shouldForwardMessagesAfterAuth() {
EmbeddedChannel channel = new EmbeddedChannel(handler);
// 第一次:握手包
String handshake = AuthHandshakeHandler.HANDSHAKE_PREFIX + TOKEN
+ AuthHandshakeHandler.HANDSHAKE_SUFFIX;
channel.writeInbound(Unpooled.copiedBuffer(handshake, StandardCharsets.UTF_8));
// handler 已移除,后续消息应直接走 inbound pipeline
// 由于 EmbeddedChannel 没有 AgentDecoderHandler,消息会到 inbound queue
ByteBuf businessMsg = Unpooled.copiedBuffer("business-data", StandardCharsets.UTF_8);
channel.writeInbound(businessMsg);
// 读取 inbound queue:应有业务消息(握手包被 handler 消费了)
Object inbound = channel.readInbound();
assertNotNull(inbound, "已认证后业务消息应被放行");
channel.finishAndReleaseAll();
}
@Test
@DisplayName("新连接应记录客户端 IP 并标记未认证")
void shouldRecordClientIpOnConnect() {
EmbeddedChannel channel = new EmbeddedChannel(handler);
// channelActive 应该被触发(EmbeddedChannel 构造时会调用)
String clientIp = channel.attr(AuthHandshakeHandler.CLIENT_IP).get();
// EmbeddedChannel 没有 remoteAddress,但属性应被设置
assertNotNull(clientIp, "CLIENT_IP 属性应被设置");
Boolean authenticated = channel.attr(AuthHandshakeHandler.AUTHENTICATED).get();
assertEquals(false, authenticated, "初始应为未认证");
channel.finishAndReleaseAll();
}
}
@@ -0,0 +1,158 @@
package com.tongran.agent.client.security;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@link HmacSignVerifier} 单元测试
*
* <p>覆盖:正确签名错误签名过期签名未来签名格式错误未配置 secret 等场景
*
* @author Senior Developer
*/
@DisplayName("HMAC 签名校验器测试")
class HmacSignVerifierTest {
private SecurityProperties properties;
private HmacSignVerifier verifier;
private static final String SECRET = "test-secret-key-123456";
private static final String BODY = "restart.sh\n--service agent";
@BeforeEach
void setUp() {
properties = new SecurityProperties();
properties.setSignSecret(SECRET);
properties.setSignTtlSeconds(300); // 5 分钟
verifier = new HmacSignVerifier(properties);
}
@Test
@DisplayName("正确签名应校验通过")
void shouldPassWithValidSignature() {
long now = System.currentTimeMillis();
String sign = buildSign(SECRET, BODY, now);
HmacSignVerifier.VerifyResult result = verifier.verify(sign, BODY, now);
assertTrue(result.isSuccess(), "正确签名应通过");
}
@Test
@DisplayName("签名内容不匹配应拒绝")
void shouldRejectTamperedBody() {
long now = System.currentTimeMillis();
String sign = buildSign(SECRET, BODY, now);
HmacSignVerifier.VerifyResult result = verifier.verify(sign, "tampered-body", now);
assertFalse(result.isSuccess());
assertEquals("签名不匹配", result.getReason());
}
@Test
@DisplayName("过期签名应拒绝(防重放)")
void shouldRejectExpiredSignature() {
long now = System.currentTimeMillis();
long oldTimestamp = now - 600_000; // 10 分钟前,超过 TTL
String sign = buildSign(SECRET, BODY, oldTimestamp);
HmacSignVerifier.VerifyResult result = verifier.verify(sign, BODY, now);
assertFalse(result.isSuccess());
assertEquals("签名已过期", result.getReason());
}
@Test
@DisplayName("未来签名(60秒内时钟偏差)应通过")
void shouldPassWithFutureSignatureWithinClockSkew() {
long now = System.currentTimeMillis();
long futureTimestamp = now + 30_000; // 30 秒后, 60 秒偏差内
String sign = buildSign(SECRET, BODY, futureTimestamp);
HmacSignVerifier.VerifyResult result = verifier.verify(sign, BODY, now);
assertTrue(result.isSuccess(), "60秒内的时钟偏差应通过");
}
@Test
@DisplayName("远未来签名应拒绝")
void shouldRejectFarFutureSignature() {
long now = System.currentTimeMillis();
long futureTimestamp = now + 120_000; // 2 分钟后,超过 60 秒偏差
String sign = buildSign(SECRET, BODY, futureTimestamp);
HmacSignVerifier.VerifyResult result = verifier.verify(sign, BODY, now);
assertFalse(result.isSuccess());
assertEquals("签名时间戳超前过多", result.getReason());
}
@Test
@DisplayName("格式错误的签名应拒绝")
void shouldRejectMalformedSignature() {
HmacSignVerifier.VerifyResult r1 = verifier.verify(null, BODY, System.currentTimeMillis());
assertFalse(r1.isSuccess());
HmacSignVerifier.VerifyResult r2 = verifier.verify("invalid-format", BODY, System.currentTimeMillis());
assertFalse(r2.isSuccess());
HmacSignVerifier.VerifyResult r3 = verifier.verify("v1:not-a-number:abc", BODY, System.currentTimeMillis());
assertFalse(r3.isSuccess());
assertEquals("签名时间戳非法", r3.getReason());
}
@Test
@DisplayName("未配置 secret 应拒绝")
void shouldRejectWhenSecretNotConfigured() {
properties.setSignSecret("");
HmacSignVerifier verifierNoSecret = new HmacSignVerifier(properties);
long now = System.currentTimeMillis();
String sign = buildSign(SECRET, BODY, now);
HmacSignVerifier.VerifyResult result = verifierNoSecret.verify(sign, BODY, now);
assertFalse(result.isSuccess());
assertEquals("签名密钥未配置", result.getReason());
}
@Test
@DisplayName("空 body 应正常校验")
void shouldHandleNullBody() {
long now = System.currentTimeMillis();
String sign = buildSign(SECRET, "", now);
HmacSignVerifier.VerifyResult result = verifier.verify(sign, null, now);
assertTrue(result.isSuccess());
}
@Test
@DisplayName("SHA-256 工具方法应正确计算")
void shouldComputeSha256Correctly() {
byte[] data = "hello world".getBytes(StandardCharsets.UTF_8);
String sha = HmacSignVerifier.sha256Hex(data);
// 已知值:echo -n "hello world" | sha256sum
assertEquals("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
sha.toLowerCase());
}
/** 构造符合格式的签名:v1:{timestamp}:{hexSig} */
private String buildSign(String secret, String body, long timestamp) {
try {
String payload = timestamp + "\n" + body;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] raw = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
return "v1:" + timestamp + ":" + HexFormat.of().formatHex(raw);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,179 @@
package com.tongran.agent.client.security;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* {@link SecureCommandExecutor} 单元测试
*
* <p>覆盖:白名单通过/拒绝路径穿越签名校验参数传递等
* 底层 {@link SystemCommandRunner} mock,不真实执行命令
*
* @author Senior Developer
*/
@DisplayName("安全命令执行器测试")
class SecureCommandExecutorTest {
private SecurityProperties properties;
private HmacSignVerifier signVerifier;
private SystemCommandRunner commandRunner;
private SecureCommandExecutor executor;
private static final String SECRET = "test-secret";
private static final String SCRIPT_NAME = "restart.sh";
private static final List<String> ARGS = List.of("--service", "agent");
@BeforeEach
void setUp() {
properties = new SecurityProperties();
properties.setAllowedScriptsDir("/opt/tongran/scripts");
properties.setAllowedScriptNames(List.of("restart.sh", "update.sh"));
properties.setCommandTimeoutSeconds(60);
properties.setSignSecret(SECRET);
signVerifier = new HmacSignVerifier(properties);
commandRunner = mock(SystemCommandRunner.class);
executor = new SecureCommandExecutor(properties, signVerifier, commandRunner);
}
@Test
@DisplayName("合法脚本 + 正确签名 应执行成功")
void shouldExecuteValidScriptWithValidSign() throws Exception {
// mock runner 返回成功
when(commandRunner.execute(any(Path.class), anyList(), anyLong(), any(TimeUnit.class)))
.thenReturn(CompletableFuture.completedFuture(
SecureCommandExecutor.CommandResult.ok(0, "done", "")));
String sign = buildSign(SCRIPT_NAME, ARGS);
CompletableFuture<SecureCommandExecutor.CommandResult> future =
executor.execute(SCRIPT_NAME, ARGS, sign);
SecureCommandExecutor.CommandResult result = future.get();
assertTrue(result.isSuccess());
assertEquals(0, result.getExitCode());
// 验证 runner 被调用,且参数正确
ArgumentCaptor<Path> pathCaptor = ArgumentCaptor.forClass(Path.class);
verify(commandRunner).execute(pathCaptor.capture(), eq(ARGS), eq(60L), eq(TimeUnit.SECONDS));
assertEquals(Paths.get("/opt/tongran/scripts/restart.sh").normalize(),
pathCaptor.getValue().normalize());
}
@Test
@DisplayName("非白名单脚本应被拒绝")
void shouldRejectScriptNotInWhitelist() throws Exception {
String sign = buildSign("evil.sh", List.of());
CompletableFuture<SecureCommandExecutor.CommandResult> future =
executor.execute("evil.sh", List.of(), sign);
SecureCommandExecutor.CommandResult result = future.get();
assertFalse(result.isSuccess());
assertTrue(result.getReason().contains("白名单"));
verify(commandRunner, never()).execute(any(), anyList(), anyLong(), any());
}
@Test
@DisplayName("路径穿越应被拒绝")
void shouldRejectPathTraversal() throws Exception {
// 即使脚本名带 ../ 也应被 normalize 后校验失败
String maliciousName = "../../etc/passwd";
String sign = buildSign(maliciousName, List.of());
// 先把恶意名加入白名单(模拟配置失误),验证路径穿越防护仍能拦住
properties.setAllowedScriptNames(List.of(maliciousName));
CompletableFuture<SecureCommandExecutor.CommandResult> future =
executor.execute(maliciousName, List.of(), sign);
SecureCommandExecutor.CommandResult result = future.get();
assertFalse(result.isSuccess());
// 文件不存在或路径非法都会拦截
verify(commandRunner, never()).execute(any(), anyList(), anyLong(), any());
}
@Test
@DisplayName("签名缺失应被拒绝")
void shouldRejectMissingSign() throws Exception {
CompletableFuture<SecureCommandExecutor.CommandResult> future =
executor.execute(SCRIPT_NAME, ARGS, null);
SecureCommandExecutor.CommandResult result = future.get();
assertFalse(result.isSuccess());
verify(commandRunner, never()).execute(any(), anyList(), anyLong(), any());
}
@Test
@DisplayName("签名不匹配应被拒绝")
void shouldRejectWrongSign() throws Exception {
CompletableFuture<SecureCommandExecutor.CommandResult> future =
executor.execute(SCRIPT_NAME, ARGS, "v1:123:wrong-signature");
SecureCommandExecutor.CommandResult result = future.get();
assertFalse(result.isSuccess());
verify(commandRunner, never()).execute(any(), anyList(), anyLong(), any());
}
@Test
@DisplayName("空脚本名应被拒绝")
void shouldRejectEmptyScriptName() throws Exception {
String sign = buildSign("", List.of());
CompletableFuture<SecureCommandExecutor.CommandResult> future =
executor.execute("", List.of(), sign);
SecureCommandExecutor.CommandResult result = future.get();
assertFalse(result.isSuccess());
assertEquals("脚本名为空", result.getReason());
}
@Test
@DisplayName("null 脚本名应被拒绝")
void shouldRejectNullScriptName() throws Exception {
CompletableFuture<SecureCommandExecutor.CommandResult> future =
executor.execute(null, List.of(), "v1:123:abc");
SecureCommandExecutor.CommandResult result = future.get();
assertFalse(result.isSuccess());
}
@Test
@DisplayName("null args 应被当作空参数处理")
void shouldHandleNullArgs() throws Exception {
when(commandRunner.execute(any(Path.class), anyList(), anyLong(), any(TimeUnit.class)))
.thenReturn(CompletableFuture.completedFuture(
SecureCommandExecutor.CommandResult.ok(0, "", "")));
String sign = buildSign(SCRIPT_NAME, null);
CompletableFuture<SecureCommandExecutor.CommandResult> future =
executor.execute(SCRIPT_NAME, null, sign);
SecureCommandExecutor.CommandResult result = future.get();
assertTrue(result.isSuccess());
}
/** 构造签名 */
private String buildSign(String scriptName, List<String> args) {
long now = System.currentTimeMillis();
String payload = scriptName + "\n" + (args == null ? "" : String.join(" ", args));
try {
javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HmacSHA256");
mac.init(new javax.crypto.spec.SecretKeySpec(
SECRET.getBytes(java.nio.charset.StandardCharsets.UTF_8), "HmacSHA256"));
byte[] raw = mac.doFinal(payload.getBytes(java.nio.charset.StandardCharsets.UTF_8));
return "v1:" + now + ":" + java.util.HexFormat.of().formatHex(raw);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,175 @@
package com.tongran.agent.client.security;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@link SecureFileDownloader} 单元测试
*
* <p>覆盖:协议白名单域名白名单路径穿越SHA-256 校验大小限制等
* 真实发起 HTTPS/HTTP 请求测试成本高,这里重点测校验逻辑(在连接前就会拒绝)
*
* @author Senior Developer
*/
@DisplayName("安全文件下载器测试")
class SecureFileDownloaderTest {
@TempDir
Path tempDir;
private SecurityProperties properties;
private SecureFileDownloader downloader;
@BeforeEach
void setUp() {
properties = new SecurityProperties();
properties.getDownload().setAllowedProtocols(List.of("https"));
properties.getDownload().setAllowedHosts(List.of("oss.tongran.com", "files.tongran.com"));
properties.getDownload().setMaxSizeBytes(1024 * 1024); // 1MB
properties.getDownload().setSha256Required(true);
properties.getDownload().setAllowedSaveDir(tempDir.toString());
downloader = new SecureFileDownloader(properties);
}
@Test
@DisplayName("非白名单协议(http)应被拒绝")
void shouldRejectHttpProtocol() throws Exception {
CompletableFuture<SecureFileDownloader.DownloadResult> future =
downloader.download("http://oss.tongran.com/file.txt",
tempDir.resolve("file.txt").toString(),
"abc", null);
SecureFileDownloader.DownloadResult result = future.get();
assertFalse(result.isSuccess());
assertTrue(result.getReason().contains("协议"));
}
@Test
@DisplayName("非白名单域名应被拒绝")
void shouldRejectUnknownHost() throws Exception {
CompletableFuture<SecureFileDownloader.DownloadResult> future =
downloader.download("https://evil.com/file.txt",
tempDir.resolve("file.txt").toString(),
"abc", null);
SecureFileDownloader.DownloadResult result = future.get();
assertFalse(result.isSuccess());
assertTrue(result.getReason().contains("域名"));
}
@Test
@DisplayName("路径穿越应被拒绝")
void shouldRejectPathTraversal() throws Exception {
// 尝试保存到 allowed-save-dir 之外
Path escapePath = tempDir.resolve("../escape.txt").normalize();
CompletableFuture<SecureFileDownloader.DownloadResult> future =
downloader.download("https://oss.tongran.com/file.txt",
escapePath.toString(),
"abc", null);
SecureFileDownloader.DownloadResult result = future.get();
assertFalse(result.isSuccess());
assertTrue(result.getReason().contains("路径"));
}
@Test
@DisplayName("sha256-required 开启时未提供校验值应被拒绝")
void shouldRejectMissingSha256WhenRequired() throws Exception {
CompletableFuture<SecureFileDownloader.DownloadResult> future =
downloader.download("https://oss.tongran.com/file.txt",
tempDir.resolve("file.txt").toString(),
null, null);
SecureFileDownloader.DownloadResult result = future.get();
assertFalse(result.isSuccess());
assertTrue(result.getReason().contains("SHA-256"));
}
@Test
@DisplayName("sha256-required 关闭时未提供校验值应继续(到连接阶段)")
void shouldAllowMissingSha256WhenNotRequired() throws Exception {
properties.getDownload().setSha256Required(false);
CompletableFuture<SecureFileDownloader.DownloadResult> future =
downloader.download("https://oss.tongran.com/nonexistent-file.txt",
tempDir.resolve("file.txt").toString(),
null, null);
SecureFileDownloader.DownloadResult result = future.get();
// 协议域名路径都通过,会进入连接阶段(这里 DNS 解析失败或连接失败)
assertFalse(result.isSuccess());
// 不应是"未提供 SHA-256"的拒绝
assertFalse(result.getReason().contains("SHA-256"));
}
@Test
@DisplayName("空协议白名单应拒绝所有")
void shouldRejectAllWhenProtocolWhitelistEmpty() throws Exception {
properties.getDownload().setAllowedProtocols(List.of());
CompletableFuture<SecureFileDownloader.DownloadResult> future =
downloader.download("https://oss.tongran.com/file.txt",
tempDir.resolve("file.txt").toString(),
"abc", null);
SecureFileDownloader.DownloadResult result = future.get();
assertFalse(result.isSuccess());
}
@Test
@DisplayName("空域名白名单应拒绝所有")
void shouldRejectAllWhenHostWhitelistEmpty() throws Exception {
properties.getDownload().setAllowedHosts(List.of());
CompletableFuture<SecureFileDownloader.DownloadResult> future =
downloader.download("https://oss.tongran.com/file.txt",
tempDir.resolve("file.txt").toString(),
"abc", null);
SecureFileDownloader.DownloadResult result = future.get();
assertFalse(result.isSuccess());
}
@Test
@DisplayName("合法 HTTPS + 白名单域名 + 合法路径 应进入下载阶段")
void shouldProceedToDownloadWithValidRequest() throws Exception {
CompletableFuture<SecureFileDownloader.DownloadResult> future =
downloader.download("https://oss.tongran.com/nonexistent.txt",
tempDir.resolve("file.txt").toString(),
"expected-sha256", null);
SecureFileDownloader.DownloadResult result = future.get();
// 校验都过了,会真正去连 oss.tongran.com(测试环境大概率连不上)
assertFalse(result.isSuccess());
// 失败原因应是网络相关,而不是校验相关
assertFalse(result.getReason().contains("协议"));
assertFalse(result.getReason().contains("域名"));
assertFalse(result.getReason().contains("路径"));
assertFalse(result.getReason().contains("SHA-256"));
}
@Test
@DisplayName("DownloadResult 的 ok/fail 工厂方法应正确")
void downloadResultFactoryShouldWork() {
SecureFileDownloader.DownloadResult ok = SecureFileDownloader.DownloadResult.ok("/path", 100L, "abc");
assertTrue(ok.isSuccess());
assertEquals("/path", ok.getFilePath());
assertEquals(100L, ok.getSize());
assertEquals("abc", ok.getSha256());
SecureFileDownloader.DownloadResult fail = SecureFileDownloader.DownloadResult.fail("reason");
assertFalse(fail.isSuccess());
assertEquals("reason", fail.getReason());
}
}