gost通讯改为ws

This commit is contained in:
qaq
2025-06-25 10:49:06 +08:00
parent f3e9b31ed3
commit f7e82cdc8b
47 changed files with 3528 additions and 1513 deletions
@@ -3,6 +3,8 @@ package com.admin.common.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Min;
import javax.validation.constraints.Max;
@Data
public class ForwardDto {
@@ -15,4 +17,11 @@ public class ForwardDto {
@NotBlank(message = "远程地址不能为空")
private String remoteAddr;
/**
* 入口端口(可选,为空时自动分配)
*/
@Min(value = 1, message = "端口号不能小于1")
@Max(value = 65535, message = "端口号不能大于65535")
private Integer inPort;
}
@@ -3,6 +3,8 @@ package com.admin.common.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Min;
import javax.validation.constraints.Max;
@Data
public class ForwardUpdateDto {
@@ -21,4 +23,11 @@ public class ForwardUpdateDto {
@NotBlank(message = "远程地址不能为空")
private String remoteAddr;
/**
* 入口端口(可选,为空时自动分配)
*/
@Min(value = 1, message = "端口号不能小于1")
@Max(value = 65535, message = "端口号不能大于65535")
private Integer inPort;
}
@@ -13,8 +13,9 @@ public class NodeDto {
@NotBlank(message = "节点名称不能为空")
private String name;
@NotNull(message = "控制端口不能为空")
@Min(value = 1, message = "端口号必须在1-65535之间")
@Max(value = 65535, message = "端口号必须在1-65535之间")
private Integer port;
@NotBlank(message = "入口IP不能为空")
private String ip;
@NotBlank(message = "服务器ip不能为空")
private String serverIp;
}
@@ -14,6 +14,9 @@ public class NodeUpdateDto {
@NotBlank(message = "节点名称不能为空")
private String name;
@NotBlank(message = "节点IP不能为空")
@NotBlank(message = "入口IP不能为空")
private String ip;
@NotBlank(message = "服务器ip不能为空")
private String serverIp;
}
@@ -46,4 +46,10 @@ public class TunnelDto {
// 协议类型(隧道转发时使用:tls、tcp、mtls),默认为tls
private String protocol;
// TCP监听地址,默认为0.0.0.0
private String tcpListenAddr = "0.0.0.0";
// UDP监听地址,默认为0.0.0.0
private String udpListenAddr = "0.0.0.0";
}
@@ -8,6 +8,44 @@ public class TunnelListDto {
private Integer id;
private String name;
/**
* 入口IP
*/
private String ip;
/**
* 入口端口范围开始
*/
private Integer inPortSta;
/**
* 入口端口范围结束
*/
private Integer inPortEnd;
/**
* 出口IP
*/
private String outIp;
/**
* 出口端口范围开始
*/
private Integer outIpSta;
/**
* 出口端口范围结束
*/
private Integer outIpEnd;
/**
* 隧道类型(1-端口转发,2-隧道转发)
*/
private Integer type;
/**
* 协议类型
*/
private String protocol;
}
@@ -37,4 +37,10 @@ public class TunnelUpdateDto {
@Min(value = 1, message = "出口端口结束必须大于等于0")
@Max(value = 65535, message = "出口端口结束不能超过65535")
private Integer outIpEnd;
// TCP监听地址
private String tcpListenAddr;
// UDP监听地址
private String udpListenAddr;
}
@@ -12,16 +12,17 @@ import com.admin.service.SpeedLimitService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;
@Slf4j
@Configuration
@EnableScheduling
@Service
public class CheckGostConfigAsync {
@Resource
@@ -33,39 +34,17 @@ public class CheckGostConfigAsync {
@Resource
private SpeedLimitService speedLimitService;
/**
* 启动后10秒执行一次,然后每10分钟执行一次
* 清理孤立的Gost配置项
*/
@Scheduled(initialDelay = 10000, fixedRate = 600000)
public void cleanOrphanedGostConfigs() {
log.info("开始清理孤立的Gost配置项");
List<Node> activeNodes = nodeService.list(new QueryWrapper<Node>().eq("status", 1));
log.info("找到 {} 个活跃节点", activeNodes.size());
for (Node node : activeNodes) {
cleanNodeConfigs(node);
}
log.info("Gost配置清理任务完成");
}
/**
* 清理单个节点的配置
* 清理孤立的Gost配置项
*/
private void cleanNodeConfigs(Node node) {
String nodeAddress = node.getIp() + ":" + node.getPort();
try {
GostConfigDto gostConfig = GostUtil.GetConfig(nodeAddress, node.getSecret());
@Async
public void cleanNodeConfigs(String node_id, GostConfigDto gostConfig) {
Node node = nodeService.getById(node_id);
if (node != null) {
cleanOrphanedServices(gostConfig, node);
cleanOrphanedChains(gostConfig, node);
cleanOrphanedLimiters(gostConfig, node);
} catch (Exception e) {
log.error("清理节点 {} 配置时发生错误", nodeAddress, e);
}
}
@@ -76,8 +55,7 @@ public class CheckGostConfigAsync {
if (gostConfig.getServices() == null) {
return;
}
String nodeAddress = node.getIp() + ":" + node.getPort();
for (ConfigItem service : gostConfig.getServices()) {
safeExecute(() -> {
@@ -95,8 +73,15 @@ public class CheckGostConfigAsync {
if (Objects.equals(type, "tcp")) { // 只处理TCP,避免重复处理
Forward forward = forwardService.getById(forwardId);
if (forward == null) {
log.warn("删除孤立的服务: {} (节点: {})", service.getName(), nodeAddress);
GostUtil.DeleteService(nodeAddress, forwardId+"_"+userId+"_"+userTunnelId, node.getSecret());
log.warn("删除孤立的服务: {} (节点: {})", service.getName(), node.getId());
GostUtil.DeleteService(node.getId(), forwardId+"_"+userId+"_"+userTunnelId);
}
}
if (Objects.equals(type, "tls")) {
Forward forward = forwardService.getById(forwardId);
if (forward == null) {
log.warn("删除孤立的服务: {} (节点: {})", service.getName(), node.getId());
GostUtil.DeleteRemoteService(node.getId(), forwardId+"_"+userId+"_"+userTunnelId);
}
}
}
@@ -112,8 +97,7 @@ public class CheckGostConfigAsync {
return;
}
String nodeAddress = node.getIp() + ":" + node.getPort();
for (ConfigItem chain : gostConfig.getChains()) {
safeExecute(() -> {
String[] serviceIds = parseServiceName(chain.getName());
@@ -126,8 +110,8 @@ public class CheckGostConfigAsync {
if (Objects.equals(type, "chains")) {
Forward forward = forwardService.getById(forwardId);
if (forward == null) {
log.warn("删除孤立的链: {} (节点: {})", chain.getName(), nodeAddress);
GostUtil.DeleteChains(nodeAddress, forwardId+"_"+userId+"_"+userTunnelId, node.getSecret());
log.warn("删除孤立的链: {} (节点: {})", chain.getName(), node.getId());
GostUtil.DeleteChains(node.getId(), forwardId+"_"+userId+"_"+userTunnelId);
}
}
}
@@ -143,14 +127,13 @@ public class CheckGostConfigAsync {
return;
}
String nodeAddress = node.getIp() + ":" + node.getPort();
for (ConfigItem limiter : gostConfig.getLimiters()) {
safeExecute(() -> {
SpeedLimit speedLimit = speedLimitService.getById(limiter.getName());
if (speedLimit == null) {
log.warn("删除孤立的限流器: {} (节点: {})", limiter.getName(), nodeAddress);
GostUtil.DeleteLimiters(nodeAddress, Long.parseLong(limiter.getName()), node.getSecret());
log.warn("删除孤立的限流器: {} (节点: {})", limiter.getName(), node.getId());
GostUtil.DeleteLimiters(node.getId(), Long.parseLong(limiter.getName()));
}
}, "清理限流器 " + limiter.getName());
}
@@ -235,17 +235,15 @@ public class DelayQueueManager implements CommandLineRunner {
}
String serviceName = buildServiceName(forward.getId(), userId, userTunnel.getId());
String nodeAddress = buildNodeAddress(inNode);
// 暂停主服务
GostDto result = GostUtil.PauseService(nodeAddress, serviceName, inNode.getSecret());
GostDto result = GostUtil.PauseService(inNode.getId(), serviceName);
// 隧道转发需要同时暂停远端服务
if (tunnel.getType() == 2) { // TUNNEL_TYPE_TUNNEL_FORWARD
Node outNode = nodeMapper.selectById(tunnel.getOutNodeId());
if (outNode != null) {
String outNodeAddress = buildNodeAddress(outNode);
GostDto remoteResult = GostUtil.PauseRemoteService(outNodeAddress, serviceName, outNode.getSecret());
GostDto remoteResult = GostUtil.PauseRemoteService(outNode.getId(), serviceName);
if (!"OK".equals(remoteResult.getMsg())) {
log.warn("暂停远端服务失败,转发ID:{},用户ID:{},服务名:{},结果:{}",
forward.getId(), userId, serviceName, remoteResult.getMsg());
@@ -297,16 +295,7 @@ public class DelayQueueManager implements CommandLineRunner {
return forwardId + "_" + userId + "_" + userTunnelId;
}
/**
* 构建节点地址
*
* @param node 节点对象
* @return 节点地址字符串
*/
private String buildNodeAddress(Node node) {
return node.getIp() + ":" + node.getPort();
}
/**
* 初始化用户账号到期延时任务
* 查询所有非管理员的正常用户,为有到期时间且未过期的用户创建延时任务
@@ -455,17 +444,15 @@ public class DelayQueueManager implements CommandLineRunner {
}
String serviceName = buildServiceName(forward.getId(), Long.valueOf(userTunnel.getUserId()), userTunnel.getId());
String nodeAddress = buildNodeAddress(inNode);
// 暂停服务
GostDto result = GostUtil.PauseService(nodeAddress, serviceName, inNode.getSecret());
GostDto result = GostUtil.PauseService(inNode.getId(), serviceName);
// 隧道转发需要同时暂停远端服务
if (tunnel.getType() == 2) { // TUNNEL_TYPE_TUNNEL_FORWARD
Node outNode = nodeMapper.selectById(tunnel.getOutNodeId());
if (outNode != null) {
String outNodeAddress = buildNodeAddress(outNode);
GostDto remoteResult = GostUtil.PauseRemoteService(outNodeAddress, serviceName, outNode.getSecret());
GostDto remoteResult = GostUtil.PauseRemoteService(outNode.getId(), serviceName);
if (!"OK".equals(remoteResult.getMsg())) {
log.warn("暂停远端服务失败,转发ID:{},用户ID:{},隧道ID:{},服务名:{},结果:{}",
forward.getId(), userTunnel.getUserId(), userTunnel.getTunnelId(), serviceName, remoteResult.getMsg());
@@ -1,22 +0,0 @@
package com.admin.common.task;
import com.admin.common.dto.GostDto;
import com.admin.common.utils.GostUtil;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class SaveConfigAsync {
@Async
public void run(String addr, String secret){
try {
GostUtil.SaveConfig(addr, secret);
}catch (Exception e){
e.printStackTrace();
}
}
}
@@ -2,212 +2,99 @@ package com.admin.common.utils;
import com.admin.common.dto.GostConfigDto;
import com.admin.common.dto.GostDto;
import com.admin.entity.Tunnel;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.aspectj.apache.bcel.generic.RET;
import java.util.Objects;
public class GostUtil {
private static final String API_BASE_URL = "/api/config/";
private static final String LIMITERS_ENDPOINT = "limiters";
private static final String SERVICES_ENDPOINT = "services";
private static final String CHAINS_ENDPOINT = "chains";
public static GostDto SaveConfig(String addr, String secret) {
JSONObject data = new JSONObject();
data.put("format", "json");
if (!addr.contains("[") && addr.indexOf(':') != addr.lastIndexOf(':')) {
// 这是IPv6地址,找到最后一个冒号(端口分隔符)
int lastColonIndex = addr.lastIndexOf(':');
String ipPart = addr.substring(0, lastColonIndex);
String portPart = addr.substring(lastColonIndex);
addr = "[" + ipPart + "]" + portPart;
}
String url = "https://" + addr + "/api/config?format=json";
return HttpUtils.post(url, data, secret);
}
public static GostConfigDto GetConfig(String addr, String secret) {
JSONObject data = new JSONObject();
data.put("format", "json");
if (!addr.contains("[") && addr.indexOf(':') != addr.lastIndexOf(':')) {
// 这是IPv6地址,找到最后一个冒号(端口分隔符)
int lastColonIndex = addr.lastIndexOf(':');
String ipPart = addr.substring(0, lastColonIndex);
String portPart = addr.substring(lastColonIndex);
addr = "[" + ipPart + "]" + portPart;
}
String url = "https://" + addr + "/api/config?format=json";
return HttpUtils.get(url, secret);
}
/**
* 添加限流器配置
*
* @param addr 服务器地址
* @param name 限流器名称
* @param speed 限速值(MB)
* @param secret 认证密钥
* @return 请求结果
*/
public static GostDto AddLimiters(String addr, Long name, String speed, String secret) {
public static GostDto AddLimiters(Long node_id, Long name, String speed) {
JSONObject data = createLimiterData(name, speed);
String url = buildUrl(addr, LIMITERS_ENDPOINT);
return HttpUtils.post(url, data, secret);
return WebSocketServer.send_msg(node_id, data, "AddLimiters");
}
/**
* 更新限流器配置
*
* @param addr 服务器地址
* @param name 限流器名称
* @param speed 限速值(MB)
* @param secret 认证密钥
* @return 请求结果
*/
public static GostDto UpdateLimiters(String addr, Long name, String speed, String secret) {
public static GostDto UpdateLimiters(Long node_id, Long name, String speed) {
JSONObject data = createLimiterData(name, speed);
String url = buildUrl(addr, LIMITERS_ENDPOINT + "/" + name);
return HttpUtils.put(url, data, secret);
JSONObject req = new JSONObject();
req.put("limiter", name + "");
req.put("data", data);
return WebSocketServer.send_msg(node_id, req, "UpdateLimiters");
}
/**
* 删除限流器配置
*
* @param addr 服务器地址
* @param name 限流器名称
* @param secret 认证密钥
* @return 请求结果
*/
public static GostDto DeleteLimiters(String addr, Long name, String secret) {
String url = buildUrl(addr, LIMITERS_ENDPOINT + "/" + name);
return HttpUtils.delete(url, secret);
public static GostDto DeleteLimiters(Long node_id, Long name) {
JSONObject req = new JSONObject();
req.put("limiter", name + "");
return WebSocketServer.send_msg(node_id, req, "DeleteLimiters");
}
/**
* 创建限流器数据
*/
private static JSONObject createLimiterData(Long name, String speed) {
JSONObject data = new JSONObject();
data.put("name", name.toString());
JSONArray limits = new JSONArray();
limits.add("$ " + speed + "MB " + speed + "MB");
data.put("limits", limits);
return data;
}
/**
* 添加服务配置(支持端口转发和隧道转发)
*
* @param addr 服务器地址
* @param name 服务名称
* @param in_port 监听端口
* @param limiter 限流器ID
* @param remoteAddr 远程地址(端口转发时使用)
* @param secret 认证密钥
* @param fow_type 转发类型:1=端口转发,2=隧道转发
* @return 请求结果
*/
public static GostDto AddService(String addr, String name, Integer in_port, Integer limiter, String remoteAddr, String secret, Integer fow_type) {
public static GostDto AddService(Long node_id, String name, Integer in_port, Integer limiter, String remoteAddr, Integer fow_type, Tunnel tunnel) {
JSONArray services = new JSONArray();
String[] protocols = {"tcp", "udp"};
for (String protocol : protocols) {
JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type);
JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type, tunnel);
services.add(service);
}
String url = buildUrl(addr, SERVICES_ENDPOINT + "/batch");
return HttpUtils.post(url, services, secret);
return WebSocketServer.send_msg(node_id, services, "AddService");
}
/**
* 更新服务配置(批量更新TCP和UDP服务)
*
* @param addr 服务器地址
* @param name 服务名称
* @param in_port 监听端口
* @param limiter 限流器ID
* @param remoteAddr 远程地址(端口转发时使用)
* @param secret 认证密钥
* @param fow_type 转发类型:1=端口转发,2=隧道转发
* @return 请求结果
*/
public static GostDto UpdateService(String addr, String name, Integer in_port, Integer limiter, String remoteAddr, String secret, Integer fow_type) {
public static GostDto UpdateService(Long node_id, String name, Integer in_port, Integer limiter, String remoteAddr, Integer fow_type, Tunnel tunnel) {
JSONArray services = new JSONArray();
String[] protocols = {"tcp", "udp"};
for (String protocol : protocols) {
JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type);
JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type, tunnel);
services.add(service);
}
String url = buildUrl(addr, SERVICES_ENDPOINT + "/batch");
return HttpUtils.put(url, services, secret);
return WebSocketServer.send_msg(node_id, services, "UpdateService");
}
/**
* 删除服务配置(批量删除TCP和UDP服务)
*
* @param addr 服务器地址
* @param name 服务名称
* @param secret 认证密钥
* @return 请求结果
*/
public static GostDto DeleteService(String addr, String name, String secret) {
public static GostDto DeleteService(Long node_id, String name) {
JSONObject data = new JSONObject();
JSONArray services = new JSONArray();
services.add(name + "_tcp");
services.add(name + "_udp");
data.put("services", services);
String url = buildUrl(addr, SERVICES_ENDPOINT + "/batch");
return HttpUtils.delete(url, data, secret);
return WebSocketServer.send_msg(node_id, data, "DeleteService");
}
public static GostDto PauseService(String addr, String name, String secret) {
public static GostDto PauseService(Long node_id, String name) {
JSONObject data = new JSONObject();
JSONArray services = new JSONArray();
services.add(name + "_tcp");
services.add(name + "_udp");
data.put("services", services);
String url = buildUrl(addr, SERVICES_ENDPOINT + "/batch/pause");
return HttpUtils.post(url, data, secret);
return WebSocketServer.send_msg(node_id, data, "PauseService");
}
public static GostDto ResumeService(String addr, String name, String secret) {
public static GostDto ResumeService(Long node_id, String name) {
JSONObject data = new JSONObject();
JSONArray services = new JSONArray();
services.add(name + "_tcp");
services.add(name + "_udp");
data.put("services", services);
String url = buildUrl(addr, SERVICES_ENDPOINT + "/batch/resume");
return HttpUtils.post(url, data, secret);
return WebSocketServer.send_msg(node_id, data, "ResumeService");
}
public static GostDto PauseRemoteService(String addr, String name, String secret) {
public static GostDto PauseRemoteService(Long node_id, String name) {
JSONObject data = new JSONObject();
JSONArray services = new JSONArray();
services.add(name + "_tls");
data.put("services", services);
String url = buildUrl(addr, SERVICES_ENDPOINT + "/batch/pause");
return HttpUtils.post(url, data, secret);
return WebSocketServer.send_msg(node_id, data, "PauseRemoteService");
}
public static GostDto ResumeRemoteService(String addr, String name, String secret) {
public static GostDto ResumeRemoteService(Long node_id, String name) {
JSONObject data = new JSONObject();
JSONArray services = new JSONArray();
services.add(name + "_tls");
data.put("services", services);
String url = buildUrl(addr, SERVICES_ENDPOINT + "/batch/resume");
return HttpUtils.post(url, data, secret);
return WebSocketServer.send_msg(node_id, data, "ResumeRemoteService");
}
public static GostDto AddChains(String addr, String name, String remoteAddr, String secret, String protocol) {
public static GostDto AddChains(Long node_id, String name, String remoteAddr, String protocol) {
JSONObject dialer = new JSONObject();
dialer.put("type", protocol);
@@ -234,11 +121,10 @@ public class GostUtil {
data.put("name", name + "_chains");
data.put("hops", hops);
String url = buildUrl(addr, CHAINS_ENDPOINT);
return HttpUtils.post(url, data, secret);
return WebSocketServer.send_msg(node_id, data, "AddChains");
}
public static GostDto UpdateChains(String addr, String name, String remoteAddr, String secret, String protocol) {
public static GostDto UpdateChains(Long node_id, String name, String remoteAddr, String protocol) {
JSONObject dialer = new JSONObject();
dialer.put("type", protocol);
@@ -264,18 +150,19 @@ public class GostUtil {
JSONObject data = new JSONObject();
data.put("name", name + "_chains");
data.put("hops", hops);
String url = buildUrl(addr, CHAINS_ENDPOINT + "/" + name + "_chains");
return HttpUtils.put(url, data, secret);
JSONObject req = new JSONObject();
req.put("chain", name + "_chains");
req.put("data", data);
return WebSocketServer.send_msg(node_id, req, "UpdateChains");
}
public static GostDto DeleteChains(String addr, String name, String secret) {
String url = buildUrl(addr, CHAINS_ENDPOINT + "/" + name + "_chains");
return HttpUtils.delete(url, secret);
public static GostDto DeleteChains(Long node_id, String name) {
JSONObject data = new JSONObject();
data.put("chain", name + "_chains");
return WebSocketServer.send_msg(node_id, data, "DeleteChains");
}
public static GostDto AddRemoteService(String addr, String name, Integer out_port, String remoteAddr, String secret, String protocol) {
public static GostDto AddRemoteService(Long node_id, String name, Integer out_port, String remoteAddr, String protocol) {
JSONObject data = new JSONObject();
data.put("name", name + "_tls");
data.put("addr", ":" + out_port);
@@ -293,12 +180,12 @@ public class GostUtil {
nodes.add(node);
forwarder.put("nodes", nodes);
data.put("forwarder", forwarder);
String url = buildUrl(addr, SERVICES_ENDPOINT);
return HttpUtils.post(url, data, secret);
JSONArray services = new JSONArray();
services.add(data);
return WebSocketServer.send_msg(node_id, services, "AddService");
}
public static GostDto UpdateRemoteService(String addr, String name, Integer out_port, String remoteAddr, String secret) {
public static GostDto UpdateRemoteService(Long node_id, String name, Integer out_port, String remoteAddr) {
JSONObject data = new JSONObject();
data.put("name", name + "_tls");
data.put("addr", ":" + out_port);
@@ -316,24 +203,36 @@ public class GostUtil {
nodes.add(node);
forwarder.put("nodes", nodes);
data.put("forwarder", forwarder);
String url = buildUrl(addr, SERVICES_ENDPOINT + "/" + name + "_tls");
return HttpUtils.put(url, data, secret);
JSONArray services = new JSONArray();
services.add(data);
return WebSocketServer.send_msg(node_id, services, "UpdateService");
}
public static GostDto DeleteRemoteService(String addr, String name, String secret) {
String url = buildUrl(addr, SERVICES_ENDPOINT + "/" + name + "_tls");
return HttpUtils.delete(url, secret);
public static GostDto DeleteRemoteService(Long node_id, String name) {
JSONArray data = new JSONArray();
data.add(name + "_tls");
JSONObject req = new JSONObject();
req.put("services", data);
return WebSocketServer.send_msg(node_id, req, "DeleteService");
}
private static JSONObject createLimiterData(Long name, String speed) {
JSONObject data = new JSONObject();
data.put("name", name.toString());
JSONArray limits = new JSONArray();
limits.add("$ " + speed + "MB " + speed + "MB");
data.put("limits", limits);
return data;
}
/**
* 创建单个服务配置
*/
private static JSONObject createServiceConfig(String name, Integer in_port, Integer limiter, String remoteAddr, String protocol, Integer fow_type) {
private static JSONObject createServiceConfig(String name, Integer in_port, Integer limiter, String remoteAddr, String protocol, Integer fow_type, Tunnel tunnel) {
JSONObject service = new JSONObject();
service.put("name", name + "_" + protocol);
service.put("addr", ":" + in_port);
if (Objects.equals(protocol, "tcp")){
service.put("addr", tunnel.getTcpListenAddr() + ":" + in_port);
}else {
service.put("addr", tunnel.getUdpListenAddr() + ":" + in_port);
}
// 添加限流器配置
if (limiter != null) {
@@ -357,9 +256,6 @@ public class GostUtil {
return service;
}
/**
* 创建处理器配置
*/
private static JSONObject createHandler(String protocol, String name, Integer fow_type) {
JSONObject handler = new JSONObject();
handler.put("type", protocol);
@@ -372,18 +268,12 @@ public class GostUtil {
return handler;
}
/**
* 创建监听器配置
*/
private static JSONObject createListener(String protocol) {
JSONObject listener = new JSONObject();
listener.put("type", protocol);
return listener;
}
/**
* 创建转发器配置
*/
private static JSONObject createForwarder(String protocol, String remoteAddr) {
JSONObject forwarder = new JSONObject();
JSONArray nodes = new JSONArray();
@@ -395,33 +285,12 @@ public class GostUtil {
return forwarder;
}
/**
* 判断是否为端口转发
*/
private static boolean isPortForwarding(Integer fow_type) {
return fow_type != null && fow_type == 1;
}
/**
* 判断是否为隧道转发
*/
private static boolean isTunnelForwarding(Integer fow_type) {
return fow_type != null && fow_type != 1;
}
/**
* 构建API URL
*/
private static String buildUrl(String addr, String endpoint) {
// 如果是IPv6地址(包含多个冒号且不包含方括号),需要用方括号包裹IP部分
if (!addr.contains("[") && addr.indexOf(':') != addr.lastIndexOf(':')) {
// 这是IPv6地址,找到最后一个冒号(端口分隔符)
int lastColonIndex = addr.lastIndexOf(':');
String ipPart = addr.substring(0, lastColonIndex);
String portPart = addr.substring(lastColonIndex);
addr = "[" + ipPart + "]" + portPart;
}
return "https://" + addr + API_BASE_URL + endpoint;
}
}
@@ -2,24 +2,9 @@ package com.admin.common.utils;
import com.admin.common.dto.GostConfigDto;
import com.admin.common.dto.GostDto;
import com.admin.common.task.SaveConfigAsync;
import com.admin.config.RestTemplateConfig;
import com.alibaba.fastjson.JSONObject;
import lombok.SneakyThrows;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
@@ -29,7 +14,6 @@ import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import org.springframework.util.StreamUtils;
import java.io.IOException;
import java.net.URI;
@@ -41,251 +25,7 @@ import java.util.*;
* 支持GET和POST请求,支持表单和JSON格式的请求体
*/
@Component
public class HttpUtils implements ApplicationContextAware {
private static final Logger logger = LoggerFactory.getLogger(HttpUtils.class);
// 10秒超时配置
private static final int TIMEOUT_SECONDS = 10;
private static final int TIMEOUT_MILLISECONDS = TIMEOUT_SECONDS * 1000;
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext context) {
HttpUtils.applicationContext = context;
}
/**
* 获取SaveConfigAsync Bean
*/
private static SaveConfigAsync getSaveConfigAsync() {
try {
return applicationContext.getBean(SaveConfigAsync.class);
} catch (Exception e) {
logger.warn("无法获取SaveConfigAsync Bean: {}", e.getMessage());
return null;
}
}
/**
* 从URL中提取IP和端口
*/
private static String extractIpAndPortFromUrl(String url) {
try {
URI uri = URI.create(url);
String host = uri.getHost();
int port = uri.getPort();
if (port == -1) {
port = uri.getScheme().equals("https") ? 443 : 80;
}
return host + ":" + port;
} catch (Exception e) {
logger.warn("无法从URL提取IP和端口: {}", url);
return "";
}
}
/**
* 异步保存配置
*/
private static void asyncSaveConfig(String url, String secret) {
try {
SaveConfigAsync saveConfigAsync = getSaveConfigAsync();
if (saveConfigAsync != null) {
String ipAndPort = extractIpAndPortFromUrl(url);
saveConfigAsync.run(ipAndPort, secret);
}
} catch (Exception e) {
logger.warn("异步保存配置失败: {}", e.getMessage());
}
}
/**
* 自定义错误处理器,不抛出异常,允许获取所有状态码的响应
*/
private static class NoOpResponseErrorHandler implements ResponseErrorHandler {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
// 返回 false,让 RestTemplate 不认为任何状态码是错误
// 这样就可以正常获取 4xx 和 5xx 的响应体
return false;
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
}
}
/**
* 创建带超时配置的RestTemplate
*/
@SneakyThrows
private static RestTemplate createRestTemplateWithTimeout() {
// 创建RestTemplate
RestTemplate restTemplate = new RestTemplate(RestTemplateConfig.generateHttpRequestFactory());
restTemplate.setErrorHandler(new NoOpResponseErrorHandler());
return restTemplate;
}
public class HttpUtils{
@SneakyThrows
public static GostConfigDto get(String url, String secret) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
String auth = secret + ":" + secret;
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
headers.set("Authorization", "Basic " + encodedAuth);
RestTemplate restTemplate = createRestTemplateWithTimeout();
HttpEntity<Object> entity = new HttpEntity<>("", headers);
try {
ResponseEntity<GostConfigDto> response = restTemplate.exchange(
url,
HttpMethod.GET,
entity,
GostConfigDto.class
);
return response.getBody();
} catch (Exception e) {
e.printStackTrace();
GostConfigDto gostDto = new GostConfigDto();
return gostDto;
}
}
@SneakyThrows
public static GostDto post(String url, Object requestBody, String secret) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
String auth = secret + ":" + secret;
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
headers.set("Authorization", "Basic " + encodedAuth);
HttpEntity<Object> entity = new HttpEntity<>(requestBody, headers);
RestTemplate restTemplate = createRestTemplateWithTimeout();
try {
ResponseEntity<GostDto> response = restTemplate.postForEntity(url, entity, GostDto.class);
GostDto body = response.getBody();
if (body.getMsg() != null && body.getMsg().contains("exists")) {
body.setMsg("OK");
}
if (!url.contains("/api/config?format=json")) {
asyncSaveConfig(url, secret);
}
return body;
} catch (Exception e) {
e.printStackTrace();
GostDto gostDto = new GostDto();
gostDto.setCode(500);
gostDto.setMsg("请求失败");
return gostDto;
}
}
@SneakyThrows
public static GostDto put(String url, Object requestBody, String secret) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
String auth = secret + ":" + secret;
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
headers.set("Authorization", "Basic " + encodedAuth);
HttpEntity<Object> entity = new HttpEntity<>(requestBody, headers);
RestTemplate restTemplate = createRestTemplateWithTimeout();
try {
ResponseEntity<GostDto> response = restTemplate.exchange(
url,
HttpMethod.PUT,
entity,
GostDto.class
);
GostDto body = response.getBody();
asyncSaveConfig(url, secret);
return body;
} catch (Exception e) {
e.printStackTrace();
GostDto gostDto = new GostDto();
gostDto.setCode(500);
gostDto.setMsg("请求失败");
return gostDto;
}
}
@SneakyThrows
public static GostDto delete(String url, String secret) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
// Basic Auth
String auth = secret + ":" + secret;
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
headers.set("Authorization", "Basic " + encodedAuth);
HttpEntity<Void> entity = new HttpEntity<>(headers);
RestTemplate restTemplate = createRestTemplateWithTimeout();
try {
ResponseEntity<GostDto> response = restTemplate.exchange(
url,
HttpMethod.DELETE,
entity,
GostDto.class
);
GostDto body = response.getBody();
if (body != null && body.getMsg() != null && body.getMsg().contains("not found")) {
body.setMsg("OK");
}
asyncSaveConfig(url, secret);
return body;
} catch (Exception e) {
e.printStackTrace();
GostDto gostDto = new GostDto();
gostDto.setCode(500);
gostDto.setMsg("请求失败");
return gostDto;
}
}
@SneakyThrows
public static GostDto delete(String url, JSONObject data, String secret) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
// Basic Auth
String auth = secret + ":" + secret;
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
headers.set("Authorization", "Basic " + encodedAuth);
HttpEntity<JSONObject> entity = new HttpEntity<>(data, headers);
RestTemplate restTemplate = createRestTemplateWithTimeout();
try {
ResponseEntity<GostDto> response = restTemplate.exchange(
url,
HttpMethod.DELETE,
entity,
GostDto.class
);
GostDto body = response.getBody();
if (body != null && body.getMsg() != null && body.getMsg().contains("not found")) {
body.setMsg("OK");
}
asyncSaveConfig(url, secret);
return body;
} catch (Exception e) {
e.printStackTrace();
GostDto gostDto = new GostDto();
gostDto.setCode(500);
gostDto.setMsg("请求失败");
return gostDto;
}
}
}
@@ -62,7 +62,7 @@ public class JwtUtil {
payload.put("iat", now.getTime() / 1000); // 发布时间
payload.put("exp", expireDate.getTime() / 1000); // 过期时间
payload.put("user", user.getUser());
payload.put("name", user.getName());
payload.put("name", user.getUser());
payload.put("role_id", user.getRoleId());
String payloadJson = JSON.toJSONString(payload);
@@ -1,6 +1,9 @@
package com.admin.common.utils;
import com.admin.common.dto.GostConfigDto;
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.JSONObject;
@@ -14,8 +17,11 @@ import org.springframework.web.socket.handler.TextWebSocketHandler;
import javax.annotation.Resource;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.UUID;
@Slf4j
@@ -24,25 +30,62 @@ public class WebSocketServer extends TextWebSocketHandler {
@Resource
NodeService nodeService;
// 存储所有活跃的 WebSocket 连接
@Resource
CheckGostConfigAsync checkGostConfigAsync;
// 存储所有活跃的 WebSocket 连接(
private static final CopyOnWriteArraySet<WebSocketSession> activeSessions = new CopyOnWriteArraySet<>();
// 存储节点ID和对应的WebSocket session映射
private static final ConcurrentHashMap<Long, WebSocketSession> nodeSessions = new ConcurrentHashMap<>();
// 为每个session提供锁对象,防止并发发送消息
private static final ConcurrentHashMap<String, Object> sessionLocks = new ConcurrentHashMap<>();
// 存储等待响应的请求,key为requestId,value为CompletableFuture
private static final ConcurrentHashMap<String, CompletableFuture<GostDto>> pendingRequests = new ConcurrentHashMap<>();
//接受客户端消息
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
try {
if (StringUtils.isNoneBlank(message.getPayload())) {
//log.info("收到消息: {}", message.getPayload());
String id = session.getAttributes().get("id").toString();
String type = session.getAttributes().get("type").toString();
// 先发送确认消息
sendToUser(session, "ok");
if (message.getPayload().contains("memory_usage")){
// 先发送确认消息
sendToUser(session, "{\"type\":\"call\"}");
} else if (message.getPayload().contains("config_report")) {
log.info("收到消息: {}", message.getPayload());
JSONObject jsonObject = JSONObject.parseObject(message.getPayload());
String string = jsonObject.getString("data");
GostConfigDto gostConfigDto = JSONObject.parseObject(string, GostConfigDto.class);
checkGostConfigAsync.cleanNodeConfigs(id, gostConfigDto);
} else if (message.getPayload().contains("requestId")) {
log.info("收到消息: {}", message.getPayload());
// 处理命令响应消息
try {
JSONObject responseJson = JSONObject.parseObject(message.getPayload());
String requestId = responseJson.getString("requestId");
String responseMessage = responseJson.getString("message");
if (requestId != null) {
CompletableFuture<GostDto> future = pendingRequests.remove(requestId);
if (future != null) {
GostDto result = new GostDto();
result.setMsg(responseMessage != null ? responseMessage : "无响应消息");
future.complete(result);
}
}
} catch (Exception e) {
log.error("处理响应消息失败: {}", e.getMessage(), e);
}
} else {
log.info("收到消息: {}", message.getPayload());
}
// 如果是节点类型,转发消息给其他会话
if (Objects.equals(type, "1")) {
JSONObject jsonObject = new JSONObject();
@@ -70,21 +113,30 @@ public class WebSocketServer extends TextWebSocketHandler {
try {
String id = session.getAttributes().get("id").toString();
String type = session.getAttributes().get("type").toString();
if (!Objects.equals(type, "1")) {
// 网页管理员连接
activeSessions.add(session);
}else {
Node byId = nodeService.getById(id);
} else {
// 客户端节点连接
Long nodeId = Long.valueOf(id);
nodeSessions.put(nodeId, session);
// 更新节点状态为在线
Node byId = nodeService.getById(nodeId);
if (byId != null) {
byId.setStatus(1);
nodeService.updateById(byId);
// 广播节点上线状态给所有管理员
JSONObject res = new JSONObject();
res.put("id", id);
res.put("type", "status");
res.put("data", 1);
broadcastMessage(res.toJSONString());
}
}
log.info("WebSocket 连接建立成功 - id: {}, type: {}, 当前连接数: {}", id, type, activeSessions.size());
} catch (Exception e) {
log.error("建立连接时发生异常: {}", e.getMessage(), e);
@@ -100,25 +152,35 @@ public class WebSocketServer extends TextWebSocketHandler {
String sessionId = session.getId();
if (!Objects.equals(type, "1")) {
// 连接关闭
activeSessions.remove(session);
}else {
Node byId = nodeService.getById(id);
} else {
// 客户端节点连接关闭
Long nodeId = Long.valueOf(id);
nodeSessions.remove(nodeId);
// 更新节点状态为离线
Node byId = nodeService.getById(nodeId);
if (byId != null) {
byId.setStatus(0);
nodeService.updateById(byId);
JSONObject res = new JSONObject();
res.put("id", id);
res.put("type", "status");
res.put("data", 0);
broadcastMessage(res.toJSONString());
}
}
// 清理session锁对象
sessionLocks.remove(sessionId);
log.info("WebSocket 连接关闭 - id: {}, sessionId: {}, 关闭状态: {}, 当前连接数: {}",
id, sessionId, status, activeSessions.size());
// 清理该节点的待处理请求
if (Objects.equals(type, "1")) {
clearPendingRequestsForNode(Long.valueOf(id));
}
} catch (Exception e) {
log.error("关闭连接时发生异常: {}", e.getMessage(), e);
@@ -139,15 +201,34 @@ public class WebSocketServer extends TextWebSocketHandler {
}
} catch (Exception e) {
log.error("发送WebSocket消息失败 [sessionId={}]: {}", sessionId, e.getMessage());
activeSessions.remove(socketSession);
sessionLocks.remove(sessionId);
cleanupSession(socketSession);
}
}
} else {
activeSessions.remove(socketSession);
if (socketSession != null) {
sessionLocks.remove(socketSession.getId());
}
cleanupSession(socketSession);
}
}
/**
* 清理失效的session,自动识别是节点session还是管理员session
*/
private static void cleanupSession(WebSocketSession session) {
if (session == null) return;
String sessionId = session.getId();
// 清理session锁
sessionLocks.remove(sessionId);
boolean removedFromAdmin = activeSessions.remove(session);
if (!removedFromAdmin) {
nodeSessions.entrySet().removeIf(entry -> {
if (entry.getValue() == session) {
return true;
}
return false;
});
}
}
@@ -157,4 +238,69 @@ public class WebSocketServer extends TextWebSocketHandler {
sendToUser(session, message);
}
}
/**
* 清理指定节点的待处理请求
*/
private static void clearPendingRequestsForNode(Long nodeId) {
// 完成所有待处理的请求,设置为连接断开错误
pendingRequests.entrySet().removeIf(entry -> {
CompletableFuture<GostDto> future = entry.getValue();
if (!future.isDone()) {
GostDto errorResult = new GostDto();
errorResult.setMsg("节点连接已断开");
future.complete(errorResult);
}
return true; // 移除所有请求
});
}
public static GostDto send_msg(Long node_id, Object msg, String type) {
WebSocketSession nodeSession = nodeSessions.get(node_id);
if (nodeSession == null) {
GostDto result = new GostDto();
result.setMsg("节点不在线");
return result;
}
if (!nodeSession.isOpen()) {
nodeSessions.remove(node_id);
sessionLocks.remove(nodeSession.getId());
GostDto result = new GostDto();
result.setMsg("节点连接已断开");
return result;
}
// 生成唯一的请求ID
String requestId = UUID.randomUUID().toString();
// 创建CompletableFuture用于等待响应
CompletableFuture<GostDto> future = new CompletableFuture<>();
pendingRequests.put(requestId, future);
try {
JSONObject data = new JSONObject();
data.put("type", type);
data.put("data", msg);
data.put("requestId", requestId);
sendToUser(nodeSession, data.toJSONString());
GostDto result = future.get(10, TimeUnit.SECONDS);
return result;
} catch (Exception e) {
pendingRequests.remove(requestId);
GostDto result = new GostDto();
if (e instanceof java.util.concurrent.TimeoutException) {
result.setMsg("等待响应超时");
} else {
result.setMsg("发送消息失败: " + e.getMessage());
}
log.error("发送消息到节点{}失败: {}", node_id, e.getMessage(), e);
return result;
}
}
}