mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-29 16:06:36 +08:00
去除代理头支持, 增加节点面板通信加密 增加导出sql
This commit is contained in:
@@ -26,9 +26,5 @@ public class ForwardDto {
|
||||
@Min(value = 1, message = "端口号不能小于1")
|
||||
@Max(value = 65535, message = "端口号不能大于65535")
|
||||
private Integer inPort;
|
||||
|
||||
/**
|
||||
* 是否启用代理协议(0: 禁用, 1: 启用)
|
||||
*/
|
||||
private Integer proxyProtocol = 0; // 设置默认值为0(禁用)
|
||||
|
||||
}
|
||||
@@ -32,9 +32,4 @@ public class ForwardUpdateDto {
|
||||
@Min(value = 1, message = "端口号不能小于1")
|
||||
@Max(value = 65535, message = "端口号不能大于65535")
|
||||
private Integer inPort;
|
||||
|
||||
/**
|
||||
* 是否启用代理协议(0: 禁用, 1: 启用)
|
||||
*/
|
||||
private Integer proxyProtocol = 0; // 设置默认值为0(禁用)
|
||||
}
|
||||
@@ -85,9 +85,4 @@ public class ForwardWithTunnelDto {
|
||||
private Long outFlow;
|
||||
|
||||
private String strategy;
|
||||
|
||||
/**
|
||||
* 是否启用代理协议(0: 禁用, 1: 启用)
|
||||
*/
|
||||
private Integer proxyProtocol;
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package com.admin.common.utils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* AES加密工具类
|
||||
* 使用AES-256-GCM模式,与Go端保持兼容
|
||||
*/
|
||||
@Slf4j
|
||||
public class AESCrypto {
|
||||
|
||||
private static final String ALGORITHM = "AES";
|
||||
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
|
||||
private static final int GCM_IV_LENGTH = 12; // GCM推荐的IV长度
|
||||
private static final int GCM_TAG_LENGTH = 16; // GCM认证标签长度
|
||||
|
||||
private final SecretKeySpec secretKey;
|
||||
private final SecureRandom secureRandom;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param secret 密钥字符串,将使用SHA-256转换为32字节密钥
|
||||
*/
|
||||
public AESCrypto(String secret) {
|
||||
if (secret == null || secret.isEmpty()) {
|
||||
throw new IllegalArgumentException("密钥不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用SHA-256将密码转换为32字节密钥,与Go端保持一致
|
||||
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
||||
byte[] keyBytes = sha256.digest(secret.getBytes(StandardCharsets.UTF_8));
|
||||
this.secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
|
||||
this.secureRandom = new SecureRandom();
|
||||
|
||||
log.debug("AES加密器初始化成功");
|
||||
} catch (Exception e) {
|
||||
log.error("AES加密器初始化失败", e);
|
||||
throw new RuntimeException("AES加密器初始化失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密数据
|
||||
* @param data 要加密的原始数据
|
||||
* @return Base64编码的加密数据,格式为: nonce + ciphertext
|
||||
*/
|
||||
public String encrypt(byte[] data) {
|
||||
if (data == null || data.length == 0) {
|
||||
throw new IllegalArgumentException("待加密数据不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 生成随机IV(nonce)
|
||||
byte[] iv = new byte[GCM_IV_LENGTH];
|
||||
secureRandom.nextBytes(iv);
|
||||
|
||||
// 创建GCM参数规范
|
||||
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
|
||||
|
||||
// 初始化Cipher
|
||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec);
|
||||
|
||||
// 加密数据
|
||||
byte[] ciphertext = cipher.doFinal(data);
|
||||
|
||||
// 组合IV + ciphertext,与Go端格式保持一致
|
||||
ByteBuffer buffer = ByteBuffer.allocate(iv.length + ciphertext.length);
|
||||
buffer.put(iv);
|
||||
buffer.put(ciphertext);
|
||||
|
||||
// 返回Base64编码结果
|
||||
return Base64.getEncoder().encodeToString(buffer.array());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("数据加密失败", e);
|
||||
throw new RuntimeException("数据加密失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密字符串
|
||||
* @param data 要加密的字符串
|
||||
* @return Base64编码的加密数据
|
||||
*/
|
||||
public String encrypt(String data) {
|
||||
if (data == null) {
|
||||
throw new IllegalArgumentException("待加密字符串不能为空");
|
||||
}
|
||||
return encrypt(data.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密数据
|
||||
* @param encryptedData Base64编码的加密数据
|
||||
* @return 解密后的原始数据
|
||||
*/
|
||||
public byte[] decrypt(String encryptedData) {
|
||||
if (encryptedData == null || encryptedData.isEmpty()) {
|
||||
throw new IllegalArgumentException("加密数据不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// Base64解码
|
||||
byte[] encrypted = Base64.getDecoder().decode(encryptedData);
|
||||
|
||||
if (encrypted.length < GCM_IV_LENGTH) {
|
||||
throw new IllegalArgumentException("加密数据长度不足");
|
||||
}
|
||||
|
||||
// 分离IV和密文
|
||||
ByteBuffer buffer = ByteBuffer.wrap(encrypted);
|
||||
byte[] iv = new byte[GCM_IV_LENGTH];
|
||||
buffer.get(iv);
|
||||
|
||||
byte[] ciphertext = new byte[buffer.remaining()];
|
||||
buffer.get(ciphertext);
|
||||
|
||||
// 创建GCM参数规范
|
||||
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
|
||||
|
||||
// 初始化Cipher
|
||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec);
|
||||
|
||||
// 解密数据
|
||||
return cipher.doFinal(ciphertext);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("数据解密失败", e);
|
||||
throw new RuntimeException("数据解密失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密字符串
|
||||
* @param encryptedData Base64编码的加密数据
|
||||
* @return 解密后的字符串
|
||||
*/
|
||||
public String decryptString(String encryptedData) {
|
||||
byte[] decrypted = decrypt(encryptedData);
|
||||
return new String(decrypted, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建AES加密器实例
|
||||
* @param secret 密钥字符串
|
||||
* @return AES加密器实例,如果创建失败返回null
|
||||
*/
|
||||
public static AESCrypto create(String secret) {
|
||||
try {
|
||||
return new AESCrypto(secret);
|
||||
} catch (Exception e) {
|
||||
log.error("创建AES加密器失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,21 +31,21 @@ public class GostUtil {
|
||||
return WebSocketServer.send_msg(node_id, req, "DeleteLimiters");
|
||||
}
|
||||
|
||||
public static GostDto AddService(Long node_id, String name, Integer in_port, Integer limiter, String remoteAddr, Integer fow_type, Tunnel tunnel, String strategy, Integer proxy_protocol) {
|
||||
public static GostDto AddService(Long node_id, String name, Integer in_port, Integer limiter, String remoteAddr, Integer fow_type, Tunnel tunnel, String strategy) {
|
||||
JSONArray services = new JSONArray();
|
||||
String[] protocols = {"tcp", "udp"};
|
||||
for (String protocol : protocols) {
|
||||
JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type, tunnel, strategy, proxy_protocol);
|
||||
JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type, tunnel, strategy);
|
||||
services.add(service);
|
||||
}
|
||||
return WebSocketServer.send_msg(node_id, services, "AddService");
|
||||
}
|
||||
|
||||
public static GostDto UpdateService(Long node_id, String name, Integer in_port, Integer limiter, String remoteAddr, Integer fow_type, Tunnel tunnel, String strategy, Integer proxy_protocol) {
|
||||
public static GostDto UpdateService(Long node_id, String name, Integer in_port, Integer limiter, String remoteAddr, Integer fow_type, Tunnel tunnel, String strategy) {
|
||||
JSONArray services = new JSONArray();
|
||||
String[] protocols = {"tcp", "udp"};
|
||||
for (String protocol : protocols) {
|
||||
JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type, tunnel, strategy, proxy_protocol);
|
||||
JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type, tunnel, strategy);
|
||||
services.add(service);
|
||||
}
|
||||
return WebSocketServer.send_msg(node_id, services, "UpdateService");
|
||||
@@ -255,7 +255,7 @@ public class GostUtil {
|
||||
return data;
|
||||
}
|
||||
|
||||
private static JSONObject createServiceConfig(String name, Integer in_port, Integer limiter, String remoteAddr, String protocol, Integer fow_type, Tunnel tunnel, String strategy, Integer proxy_protocol) {
|
||||
private static JSONObject createServiceConfig(String name, Integer in_port, Integer limiter, String remoteAddr, String protocol, Integer fow_type, Tunnel tunnel, String strategy) {
|
||||
JSONObject service = new JSONObject();
|
||||
service.put("name", name + "_" + protocol);
|
||||
if (Objects.equals(protocol, "tcp")){
|
||||
@@ -279,13 +279,9 @@ public class GostUtil {
|
||||
|
||||
// 端口转发需要配置转发器
|
||||
if (isPortForwarding(fow_type)) {
|
||||
JSONObject forwarder = createForwarder(protocol, remoteAddr, strategy);
|
||||
JSONObject forwarder = createForwarder(remoteAddr, strategy);
|
||||
service.put("forwarder", forwarder);
|
||||
}
|
||||
JSONObject metadata = new JSONObject();
|
||||
metadata.put("proxyProtocol", proxy_protocol);
|
||||
service.put("metadata", metadata);
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
@@ -312,7 +308,7 @@ public class GostUtil {
|
||||
return listener;
|
||||
}
|
||||
|
||||
private static JSONObject createForwarder(String protocol, String remoteAddr, String strategy) {
|
||||
private static JSONObject createForwarder(String remoteAddr, String strategy) {
|
||||
JSONObject forwarder = new JSONObject();
|
||||
JSONArray nodes = new JSONArray();
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.admin.common.dto.GostDto;
|
||||
import com.admin.common.task.CheckGostConfigAsync;
|
||||
import com.admin.entity.Node;
|
||||
import com.admin.service.NodeService;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -42,6 +43,25 @@ public class WebSocketServer extends TextWebSocketHandler {
|
||||
// 存储等待响应的请求,key为requestId,value为CompletableFuture
|
||||
private static final ConcurrentHashMap<String, CompletableFuture<GostDto>> pendingRequests = new ConcurrentHashMap<>();
|
||||
|
||||
// 缓存加密器实例,避免重复创建
|
||||
private static final ConcurrentHashMap<String, AESCrypto> cryptoCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 加密消息包装器
|
||||
*/
|
||||
public static class EncryptedMessage {
|
||||
private boolean encrypted;
|
||||
private String data;
|
||||
private Long timestamp;
|
||||
|
||||
// getters and setters
|
||||
public boolean isEncrypted() { return encrypted; }
|
||||
public void setEncrypted(boolean encrypted) { this.encrypted = encrypted; }
|
||||
public String getData() { return data; }
|
||||
public void setData(String data) { this.data = data; }
|
||||
public Long getTimestamp() { return timestamp; }
|
||||
public void setTimestamp(Long timestamp) { this.timestamp = timestamp; }
|
||||
}
|
||||
|
||||
//接受客户端消息
|
||||
@Override
|
||||
@@ -51,15 +71,19 @@ public class WebSocketServer extends TextWebSocketHandler {
|
||||
|
||||
String id = session.getAttributes().get("id").toString();
|
||||
String type = session.getAttributes().get("type").toString();
|
||||
String nodeSecret = (String) session.getAttributes().get("nodeSecret");
|
||||
|
||||
// 尝试解密消息
|
||||
String decryptedPayload = decryptMessageIfNeeded(message.getPayload(), nodeSecret);
|
||||
|
||||
if (message.getPayload().contains("memory_usage")){
|
||||
if (decryptedPayload.contains("memory_usage")){
|
||||
// 先发送确认消息
|
||||
sendToUser(session, "{\"type\":\"call\"}");
|
||||
}else if (message.getPayload().contains("requestId")) {
|
||||
log.info("收到消息: {}", message.getPayload());
|
||||
sendToUser(session, "{\"type\":\"call\"}", nodeSecret);
|
||||
}else if (decryptedPayload.contains("requestId")) {
|
||||
log.info("收到消息: {}", decryptedPayload);
|
||||
// 处理命令响应消息
|
||||
try {
|
||||
JSONObject responseJson = JSONObject.parseObject(message.getPayload());
|
||||
JSONObject responseJson = JSONObject.parseObject(decryptedPayload);
|
||||
String requestId = responseJson.getString("requestId");
|
||||
String responseMessage = responseJson.getString("message");
|
||||
String responseType = responseJson.getString("type");
|
||||
@@ -91,7 +115,7 @@ public class WebSocketServer extends TextWebSocketHandler {
|
||||
log.error("处理响应消息失败: {}", e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
log.info("收到消息: {}", message.getPayload());
|
||||
log.info("收到消息: {}", decryptedPayload);
|
||||
}
|
||||
|
||||
// 如果是节点类型,转发消息给其他会话
|
||||
@@ -99,13 +123,13 @@ public class WebSocketServer extends TextWebSocketHandler {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put("id", id);
|
||||
jsonObject.put("type", "info");
|
||||
jsonObject.put("data", message.getPayload());
|
||||
jsonObject.put("data", decryptedPayload);
|
||||
String broadcastMessage = jsonObject.toJSONString();
|
||||
|
||||
// 异步处理广播消息,避免阻塞当前线程
|
||||
for (WebSocketSession targetSession : activeSessions) {
|
||||
if (targetSession != null && targetSession.isOpen() && !targetSession.equals(session)) {
|
||||
sendToUser(targetSession, broadcastMessage);
|
||||
sendToUser(targetSession, broadcastMessage, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,6 +139,78 @@ public class WebSocketServer extends TextWebSocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试解密消息(如果需要)
|
||||
*/
|
||||
private String decryptMessageIfNeeded(String payload, String nodeSecret) {
|
||||
if (payload == null || payload.trim().isEmpty()) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试解析为加密消息格式
|
||||
EncryptedMessage encryptedMessage = JSON.parseObject(payload, EncryptedMessage.class);
|
||||
|
||||
if (encryptedMessage.isEncrypted() && encryptedMessage.getData() != null) {
|
||||
// 获取或创建加密器
|
||||
AESCrypto crypto = getOrCreateCrypto(nodeSecret);
|
||||
if (crypto == null) {
|
||||
log.warn("⚠️ 收到加密消息但无法创建解密器,使用原始数据");
|
||||
return payload;
|
||||
}
|
||||
|
||||
// 解密数据
|
||||
String decryptedData = crypto.decryptString(encryptedMessage.getData());
|
||||
log.debug("🔓 WebSocket消息解密成功");
|
||||
return decryptedData;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 解析失败,可能是非加密格式,直接返回原始数据
|
||||
log.debug("WebSocket消息未加密或解密失败,使用原始数据: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密消息(如果可能)
|
||||
*/
|
||||
private static String encryptMessageIfPossible(String message, String nodeSecret) {
|
||||
if (message == null || nodeSecret == null) {
|
||||
return message;
|
||||
}
|
||||
|
||||
try {
|
||||
AESCrypto crypto = getOrCreateCrypto(nodeSecret);
|
||||
if (crypto != null) {
|
||||
String encryptedData = crypto.encrypt(message);
|
||||
|
||||
// 创建加密消息包装器
|
||||
JSONObject encryptedMessage = new JSONObject();
|
||||
encryptedMessage.put("encrypted", true);
|
||||
encryptedMessage.put("data", encryptedData);
|
||||
encryptedMessage.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
log.debug("🔐 WebSocket消息加密成功");
|
||||
return encryptedMessage.toJSONString();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("⚠️ WebSocket消息加密失败,发送原始数据: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建加密器实例
|
||||
*/
|
||||
private static AESCrypto getOrCreateCrypto(String secret) {
|
||||
if (secret == null || secret.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return cryptoCache.computeIfAbsent(secret, AESCrypto::create);
|
||||
}
|
||||
|
||||
// 建立连接
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) {
|
||||
@@ -232,7 +328,7 @@ public class WebSocketServer extends TextWebSocketHandler {
|
||||
boolean shouldUpdateOffline = true;
|
||||
try {
|
||||
// 尝试发送验证消息,如果发送成功说明连接可能还活跃
|
||||
sendToUser(session, "{\"type\":\"call\"}");
|
||||
sendToUser(session, "{\"type\":\"call\"}", null);
|
||||
log.warn("节点 {} 连接关闭但仍能发送消息,可能是假断开", nodeId);
|
||||
shouldUpdateOffline = false;
|
||||
} catch (Exception e) {
|
||||
@@ -277,6 +373,12 @@ public class WebSocketServer extends TextWebSocketHandler {
|
||||
// 点对点发送消息
|
||||
@SneakyThrows
|
||||
public static void sendToUser(WebSocketSession socketSession, String message) {
|
||||
sendToUser(socketSession, message, null);
|
||||
}
|
||||
|
||||
// 点对点发送消息(支持加密)
|
||||
@SneakyThrows
|
||||
public static void sendToUser(WebSocketSession socketSession, String message, String nodeSecret) {
|
||||
if (socketSession != null && socketSession.isOpen()) {
|
||||
String sessionId = socketSession.getId();
|
||||
Object lock = sessionLocks.computeIfAbsent(sessionId, k -> new Object());
|
||||
@@ -284,7 +386,15 @@ public class WebSocketServer extends TextWebSocketHandler {
|
||||
synchronized (lock) {
|
||||
try {
|
||||
if (socketSession.isOpen()) {
|
||||
socketSession.sendMessage(new TextMessage(message));
|
||||
// 如果是节点连接且有密钥,尝试加密消息
|
||||
String finalMessage = message;
|
||||
if (nodeSecret != null && !nodeSecret.isEmpty()) {
|
||||
String type = (String) socketSession.getAttributes().get("type");
|
||||
if ("1".equals(type)) { // 节点连接
|
||||
finalMessage = encryptMessageIfPossible(message, nodeSecret);
|
||||
}
|
||||
}
|
||||
socketSession.sendMessage(new TextMessage(finalMessage));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("发送WebSocket消息失败 [sessionId={}]: {}", sessionId, e.getMessage());
|
||||
@@ -354,13 +464,15 @@ public class WebSocketServer extends TextWebSocketHandler {
|
||||
CompletableFuture<GostDto> future = new CompletableFuture<>();
|
||||
pendingRequests.put(requestId, future);
|
||||
|
||||
// 获取节点密钥用于加密
|
||||
String nodeSecret = (String) nodeSession.getAttributes().get("nodeSecret");
|
||||
|
||||
try {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("type", type);
|
||||
data.put("data", msg);
|
||||
data.put("requestId", requestId);
|
||||
sendToUser(nodeSession, data.toJSONString());
|
||||
sendToUser(nodeSession, data.toJSONString(), nodeSecret);
|
||||
GostDto result = future.get(10, TimeUnit.SECONDS);
|
||||
|
||||
log.debug("成功发送消息到节点 {} 并收到响应: {}", node_id, result.getMsg());
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.admin.config;
|
||||
|
||||
import com.admin.common.utils.AESCrypto;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 加密配置管理类
|
||||
* 统一管理AES加密功能,为HTTP上报和WebSocket通信提供加密支持
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class EncryptionConfig {
|
||||
|
||||
// 缓存加密器实例,避免重复创建
|
||||
private static final ConcurrentHashMap<String, AESCrypto> CRYPTO_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 获取或创建AES加密器实例
|
||||
*
|
||||
* @param secret 密钥字符串
|
||||
* @return AES加密器实例,如果创建失败返回null
|
||||
*/
|
||||
public static AESCrypto getOrCreateCrypto(String secret) {
|
||||
if (secret == null || secret.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return CRYPTO_CACHE.computeIfAbsent(secret, AESCrypto::create);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测消息是否为加密格式
|
||||
*
|
||||
* @param message 消息内容
|
||||
* @return 如果是加密格式返回true,否则返回false
|
||||
*/
|
||||
public static boolean isEncryptedMessage(String message) {
|
||||
if (message == null || message.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 简单检查是否包含加密标识
|
||||
return message.contains("\"encrypted\":true") || message.contains("\"encrypted\": true");
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理缓存的加密器实例
|
||||
*
|
||||
* @param secret 要清理的密钥
|
||||
*/
|
||||
public static void clearCrypto(String secret) {
|
||||
if (secret != null) {
|
||||
CRYPTO_CACHE.remove(secret);
|
||||
log.debug("已清理密钥对应的加密器实例");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有缓存的加密器实例
|
||||
*/
|
||||
public static void clearAllCrypto() {
|
||||
int size = CRYPTO_CACHE.size();
|
||||
CRYPTO_CACHE.clear();
|
||||
log.info("已清理所有加密器实例缓存,共清理 {} 个实例", size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前缓存的加密器数量
|
||||
*
|
||||
* @return 缓存的加密器数量
|
||||
*/
|
||||
public static int getCacheSize() {
|
||||
return CRYPTO_CACHE.size();
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,15 @@ import com.admin.common.dto.FlowDto;
|
||||
import com.admin.common.dto.GostConfigDto;
|
||||
import com.admin.common.lang.R;
|
||||
import com.admin.common.task.CheckGostConfigAsync;
|
||||
import com.admin.common.utils.AESCrypto;
|
||||
import com.admin.common.utils.GostUtil;
|
||||
import com.admin.entity.*;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
@@ -41,6 +45,7 @@ import java.util.stream.Collectors;
|
||||
@RestController
|
||||
@RequestMapping("/flow")
|
||||
@CrossOrigin
|
||||
@Slf4j
|
||||
public class FlowController extends BaseController {
|
||||
|
||||
// 常量定义
|
||||
@@ -55,19 +60,51 @@ public class FlowController extends BaseController {
|
||||
private static final ConcurrentHashMap<String, Object> TUNNEL_LOCKS = new ConcurrentHashMap<>();
|
||||
private static final ConcurrentHashMap<String, Object> FORWARD_LOCKS = new ConcurrentHashMap<>();
|
||||
|
||||
// 缓存加密器实例,避免重复创建
|
||||
private static final ConcurrentHashMap<String, AESCrypto> CRYPTO_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
@Resource
|
||||
CheckGostConfigAsync checkGostConfigAsync;
|
||||
|
||||
@PostMapping("/config")
|
||||
@LogAnnotation
|
||||
public String config(@RequestBody GostConfigDto gostConfigDto, String secret) {
|
||||
Node node = nodeService.getOne(new QueryWrapper<Node>().eq("secret", secret));
|
||||
if (node == null) return SUCCESS_RESPONSE;
|
||||
checkGostConfigAsync.cleanNodeConfigs(node.getId().toString(), gostConfigDto);
|
||||
return SUCCESS_RESPONSE;
|
||||
/**
|
||||
* 加密消息包装器
|
||||
*/
|
||||
public static class EncryptedMessage {
|
||||
private boolean encrypted;
|
||||
private String data;
|
||||
private Long timestamp;
|
||||
|
||||
// getters and setters
|
||||
public boolean isEncrypted() { return encrypted; }
|
||||
public void setEncrypted(boolean encrypted) { this.encrypted = encrypted; }
|
||||
public String getData() { return data; }
|
||||
public void setData(String data) { this.data = data; }
|
||||
public Long getTimestamp() { return timestamp; }
|
||||
public void setTimestamp(Long timestamp) { this.timestamp = timestamp; }
|
||||
}
|
||||
|
||||
@PostMapping("/config")
|
||||
@LogAnnotation
|
||||
public String config(@RequestBody String rawData, String secret) {
|
||||
Node node = nodeService.getOne(new QueryWrapper<Node>().eq("secret", secret));
|
||||
if (node == null) return SUCCESS_RESPONSE;
|
||||
|
||||
try {
|
||||
// 尝试解密数据
|
||||
String decryptedData = decryptIfNeeded(rawData, secret);
|
||||
|
||||
// 解析为GostConfigDto
|
||||
GostConfigDto gostConfigDto = JSON.parseObject(decryptedData, GostConfigDto.class);
|
||||
checkGostConfigAsync.cleanNodeConfigs(node.getId().toString(), gostConfigDto);
|
||||
|
||||
log.info("🔓 节点 {} 配置数据接收成功{}", node.getId(), isEncryptedMessage(rawData) ? "(已解密)" : "");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理节点 {} 配置数据失败: {}", node.getId(), e.getMessage());
|
||||
}
|
||||
|
||||
return SUCCESS_RESPONSE;
|
||||
}
|
||||
|
||||
@RequestMapping("/test")
|
||||
@LogAnnotation
|
||||
@@ -78,32 +115,107 @@ public class FlowController extends BaseController {
|
||||
/**
|
||||
* 处理流量数据上报
|
||||
*
|
||||
* @param flowDataList 流量数据列表
|
||||
* @param secret 节点密钥
|
||||
* @param rawData 原始数据(可能是加密的)
|
||||
* @param secret 节点密钥
|
||||
* @return 处理结果
|
||||
*/
|
||||
@RequestMapping("/upload")
|
||||
@LogAnnotation
|
||||
public String uploadFlowData(@RequestBody List<FlowDto> flowDataList, String secret) {
|
||||
public String uploadFlowData(@RequestBody String rawData, String secret) {
|
||||
// 1. 验证节点权限
|
||||
if (!isValidNode(secret)) {
|
||||
return SUCCESS_RESPONSE;
|
||||
}
|
||||
if (flowDataList.isEmpty()) {
|
||||
|
||||
try {
|
||||
// 2. 尝试解密数据
|
||||
String decryptedData = decryptIfNeeded(rawData, secret);
|
||||
|
||||
// 3. 解析为FlowDto列表
|
||||
List<FlowDto> flowDataList = JSON.parseArray(decryptedData, FlowDto.class);
|
||||
|
||||
if (flowDataList.isEmpty()) {
|
||||
return SUCCESS_RESPONSE;
|
||||
}
|
||||
if (Objects.equals(flowDataList.get(0).getN(), "web_api")) {
|
||||
return SUCCESS_RESPONSE;
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
log.debug("🔓 节点流量数据接收成功{}",
|
||||
isEncryptedMessage(rawData) ? "(已解密)" : "");
|
||||
|
||||
// 4. 处理流量数据
|
||||
return processFlowData(flowDataList);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理流量数据失败: {}", e.getMessage(), e);
|
||||
return SUCCESS_RESPONSE;
|
||||
}
|
||||
if(Objects.equals(flowDataList.get(0).getN(), "web_api")){
|
||||
return SUCCESS_RESPONSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测消息是否为加密格式
|
||||
*/
|
||||
private boolean isEncryptedMessage(String data) {
|
||||
try {
|
||||
JSONObject json = JSON.parseObject(data);
|
||||
return json.getBooleanValue("encrypted");
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据需要解密数据
|
||||
*/
|
||||
private String decryptIfNeeded(String rawData, String secret) {
|
||||
if (rawData == null || rawData.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("数据不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试解析为加密消息格式
|
||||
EncryptedMessage encryptedMessage = JSON.parseObject(rawData, EncryptedMessage.class);
|
||||
|
||||
if (encryptedMessage.isEncrypted() && encryptedMessage.getData() != null) {
|
||||
// 获取或创建加密器
|
||||
AESCrypto crypto = getOrCreateCrypto(secret);
|
||||
if (crypto == null) {
|
||||
log.warn("⚠️ 收到加密消息但无法创建解密器,使用原始数据");
|
||||
return rawData;
|
||||
}
|
||||
|
||||
// 解密数据
|
||||
String decryptedData = crypto.decryptString(encryptedMessage.getData());
|
||||
log.debug("🔓 数据解密成功");
|
||||
return decryptedData;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 解析失败,可能是非加密格式,直接返回原始数据
|
||||
log.debug("数据未加密或解密失败,使用原始数据: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return rawData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建加密器实例
|
||||
*/
|
||||
private AESCrypto getOrCreateCrypto(String secret) {
|
||||
return CRYPTO_CACHE.computeIfAbsent(secret, AESCrypto::create);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理流量数据的核心逻辑
|
||||
*/
|
||||
private String processFlowData(List<FlowDto> flowDataList) {
|
||||
// 2. 解析服务名称获取ID信息
|
||||
String[] serviceIds = parseServiceName(flowDataList.get(0).getN());
|
||||
String forwardId = serviceIds[0];
|
||||
String userId = serviceIds[1];
|
||||
String userTunnelId = serviceIds[2];
|
||||
|
||||
|
||||
// 3. 一次性查询相关实体,避免后续重复查询
|
||||
Forward forward = forwardService.getById(forwardId);
|
||||
User user = userService.getById(userId);
|
||||
@@ -111,16 +223,13 @@ public class FlowController extends BaseController {
|
||||
if (!Objects.equals(userTunnelId, DEFAULT_USER_TUNNEL_ID)) {
|
||||
userTunnel = userTunnelService.getById(userTunnelId);
|
||||
}
|
||||
|
||||
// 4. 处理流量倍率
|
||||
List<FlowDto> validFlowData = filterFlowData(flowDataList, forward);
|
||||
|
||||
|
||||
|
||||
// 5. 计算总流量
|
||||
FlowStatistics flowStats = calculateTotalFlow(validFlowData);
|
||||
|
||||
|
||||
|
||||
// 6. 获取流量计费类型
|
||||
int flowType = getFlowType(forward);
|
||||
|
||||
|
||||
@@ -38,7 +38,4 @@ public class Forward extends BaseEntity{
|
||||
|
||||
private Long outFlow;
|
||||
|
||||
private Integer proxyProtocol;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -835,7 +835,7 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
|
||||
|
||||
// 创建主服务
|
||||
R serviceResult = createMainService(nodeInfo.getInNode(), serviceName, forward, limiter, tunnel.getType(), tunnel, forward.getStrategy(), forward.getProxyProtocol());
|
||||
R serviceResult = createMainService(nodeInfo.getInNode(), serviceName, forward, limiter, tunnel.getType(), tunnel, forward.getStrategy());
|
||||
if (serviceResult.getCode() != 0) {
|
||||
GostUtil.DeleteChains(nodeInfo.getInNode().getId(), serviceName);
|
||||
if (nodeInfo.getOutNode() != null) {
|
||||
@@ -1008,8 +1008,8 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
/**
|
||||
* 创建主服务
|
||||
*/
|
||||
private R createMainService(Node inNode, String serviceName, Forward forward, Integer limiter, Integer tunnelType, Tunnel tunnel, String strategy, Integer proxy_protocol) {
|
||||
GostDto result = GostUtil.AddService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy, proxy_protocol);
|
||||
private R createMainService(Node inNode, String serviceName, Forward forward, Integer limiter, Integer tunnelType, Tunnel tunnel, String strategy) {
|
||||
GostDto result = GostUtil.AddService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy);
|
||||
return isGostOperationSuccess(result) ? R.ok() : R.err(result.getMsg());
|
||||
}
|
||||
|
||||
@@ -1049,10 +1049,10 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
* 更新主服务
|
||||
*/
|
||||
private R updateMainService(Node inNode, String serviceName, Forward forward, Integer limiter, Integer tunnelType, Tunnel tunnel, String strategy) {
|
||||
GostDto result = GostUtil.UpdateService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy, forward.getProxyProtocol());
|
||||
GostDto result = GostUtil.UpdateService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy);
|
||||
|
||||
if (result.getMsg().contains(GOST_NOT_FOUND_MSG)) {
|
||||
result = GostUtil.AddService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy, forward.getProxyProtocol());
|
||||
result = GostUtil.AddService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy);
|
||||
}
|
||||
|
||||
return isGostOperationSuccess(result) ? R.ok() : R.err(result.getMsg());
|
||||
|
||||
@@ -499,7 +499,7 @@ public class UserTunnelServiceImpl extends ServiceImpl<UserTunnelMapper, UserTun
|
||||
String serviceName = buildServiceName(forward.getId(), Long.valueOf(userId), userTunnel.getId());
|
||||
|
||||
// 6. 更新入口节点的主服务限速配置(使用批量UpdateService接口)
|
||||
GostUtil.UpdateService(inNode.getId(), serviceName, forward.getInPort(), speedId, forward.getRemoteAddr(), tunnel.getType(), tunnel, forward.getStrategy(), forward.getProxyProtocol());
|
||||
GostUtil.UpdateService(inNode.getId(), serviceName, forward.getInPort(), speedId, forward.getRemoteAddr(), tunnel.getType(), tunnel, forward.getStrategy());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user