Compare commits
4
Commits
main
...
agent1.21bate
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ab9a52628 | ||
|
|
f1fa6dca7c | ||
|
|
ff7cd12fd7 | ||
|
|
546eef3372 |
+47
@@ -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 "修正脚本执行策略上报信息"
|
||||
**状态**: 优化前最后一个稳定版本,本次更新的基线
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+102
-52
@@ -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秒执行
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+79
-77
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+116
-39
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user