chore: initialize qiming workspace repository
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
package com.xspaceagi.sandbox.infra.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 反向代理服务器配置属性
|
||||
*/
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "reverse.server")
|
||||
public class ReverseServerProperties {
|
||||
|
||||
/**
|
||||
* 内部服务器配置
|
||||
*/
|
||||
private Inner inner = new Inner();
|
||||
|
||||
/**
|
||||
* 外部服务器配置
|
||||
*/
|
||||
private Outer outer = new Outer();
|
||||
|
||||
/**
|
||||
* 内部服务器配置
|
||||
*/
|
||||
@Data
|
||||
public static class Inner {
|
||||
/**
|
||||
* 内部服务器主机地址
|
||||
* 默认: 127.0.0.1
|
||||
*/
|
||||
private String serviceHost = "127.0.0.1";
|
||||
|
||||
private String bindHost = "127.0.0.1";
|
||||
|
||||
/**
|
||||
* 内部服务器端口范围
|
||||
* 格式: "起始端口-结束端口",例如 "30000-40000"
|
||||
* 默认: 30000-40000
|
||||
*/
|
||||
private String ports = "30000-40000";
|
||||
|
||||
/**
|
||||
* 解析端口范围
|
||||
*
|
||||
* @return 端口范围数组 [起始端口, 结束端口]
|
||||
*/
|
||||
public int[] parsePortRange() {
|
||||
if (ports == null || ports.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Port range cannot be empty");
|
||||
}
|
||||
|
||||
String[] parts = ports.split("-");
|
||||
if (parts.length != 2) {
|
||||
throw new IllegalArgumentException("Invalid port range format, expected 'min-max', got: " + ports);
|
||||
}
|
||||
|
||||
try {
|
||||
int minPort = Integer.parseInt(parts[0].trim());
|
||||
int maxPort = Integer.parseInt(parts[1].trim());
|
||||
|
||||
if (minPort <= 0 || maxPort <= 0) {
|
||||
throw new IllegalArgumentException("Port numbers must be positive");
|
||||
}
|
||||
|
||||
if (minPort > maxPort) {
|
||||
throw new IllegalArgumentException("Min port cannot be greater than max port");
|
||||
}
|
||||
|
||||
return new int[]{minPort, maxPort};
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Invalid port number format", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取起始端口
|
||||
*/
|
||||
public int getMinPort() {
|
||||
return parsePortRange()[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取结束端口
|
||||
*/
|
||||
public int getMaxPort() {
|
||||
return parsePortRange()[1];
|
||||
}
|
||||
|
||||
public String getServiceHost() {
|
||||
//从环境变量中获取服务器主机地址
|
||||
String envServiceHost = System.getenv("SERVICE_HOST");
|
||||
if (envServiceHost != null && !envServiceHost.trim().isEmpty()) {
|
||||
return envServiceHost;
|
||||
}
|
||||
return serviceHost;
|
||||
}
|
||||
|
||||
public String getBindHost() {
|
||||
//从环境变量中获取服务器主机地址
|
||||
String envBindHost = System.getenv("BIND_HOST");
|
||||
if (envBindHost != null && !envBindHost.trim().isEmpty()) {
|
||||
return envBindHost;
|
||||
}
|
||||
return bindHost;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 外部服务器配置
|
||||
*/
|
||||
@Data
|
||||
public static class Outer {
|
||||
/**
|
||||
* 外部服务器主机地址
|
||||
* 默认: 空(需要配置)
|
||||
*/
|
||||
private String host = "";
|
||||
|
||||
/**
|
||||
* 外部服务器端口
|
||||
* 默认: 6443
|
||||
*/
|
||||
private int port = 6443;
|
||||
|
||||
/**
|
||||
* 检查外部服务器是否已配置
|
||||
*/
|
||||
public boolean isConfigured() {
|
||||
return host != null && !host.trim().isEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.xspaceagi.sandbox.infra.config;
|
||||
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Sandbox 模块配置类
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties({
|
||||
ReverseServerProperties.class
|
||||
})
|
||||
public class SandboxConfiguration {
|
||||
// 配置类,用于启用配置属性扫描
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.xspaceagi.sandbox.infra.dao.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.xspaceagi.sandbox.spec.enums.SandboxScopeEnum;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 沙盒配置实体类
|
||||
*/
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@TableName(value = "sandbox_config", autoResultMap = true)
|
||||
public class SandboxConfig implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@TableField(value = "scope")
|
||||
private SandboxScopeEnum scope;
|
||||
|
||||
@TableField(value = "_tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
@TableField(value = "user_id")
|
||||
private Long userId;
|
||||
|
||||
@TableField(value = "name")
|
||||
private String name;
|
||||
|
||||
@TableField(value = "config_key")
|
||||
private String configKey;
|
||||
|
||||
@TableField(value = "config_value")
|
||||
private String configValue;
|
||||
|
||||
@TableField(value = "description")
|
||||
private String description;
|
||||
|
||||
@TableField(value = "is_active")
|
||||
private Boolean isActive;
|
||||
|
||||
@TableField(value = "server_info")
|
||||
private String serverInfo;
|
||||
|
||||
@TableField(value = "agent_id")
|
||||
private Long agentId;
|
||||
|
||||
@TableField(value = "max_agent")
|
||||
private Integer maxAgentCount;
|
||||
|
||||
@TableField(value = "bind_info")
|
||||
private String bindInfo;
|
||||
|
||||
@TableField(value = "type")
|
||||
private String type;
|
||||
|
||||
@TableField(value = "isolation")
|
||||
private String isolation;
|
||||
|
||||
@TableField(value = "created")
|
||||
private Date created;
|
||||
|
||||
@TableField(value = "modified")
|
||||
private Date modified;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.xspaceagi.sandbox.infra.dao.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 临时代理实体类
|
||||
*/
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@TableName(value = "sandbox_proxy", autoResultMap = true)
|
||||
public class SandboxProxy implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@TableField(value = "_tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
@TableField(value = "user_id")
|
||||
private Long userId;
|
||||
|
||||
@TableField(value = "sandbox_id")
|
||||
private Long sandboxId;
|
||||
|
||||
@TableField(value = "proxy_key")
|
||||
private String proxyKey;
|
||||
|
||||
@TableField(value = "backend_host")
|
||||
private String backendHost;
|
||||
|
||||
@TableField(value = "backend_port")
|
||||
private Integer backendPort;
|
||||
|
||||
@TableField(value = "created")
|
||||
private Date created;
|
||||
|
||||
@TableField(value = "modified")
|
||||
private Date modified;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.xspaceagi.sandbox.infra.dao.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.xspaceagi.sandbox.infra.dao.entity.SandboxConfig;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 沙盒配置 Mapper 接口
|
||||
*/
|
||||
@Mapper
|
||||
public interface SandboxConfigMapper extends BaseMapper<SandboxConfig> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.xspaceagi.sandbox.infra.dao.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.xspaceagi.sandbox.infra.dao.entity.SandboxProxy;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 临时代理 Mapper 接口
|
||||
*/
|
||||
@Mapper
|
||||
public interface SandboxProxyMapper extends BaseMapper<SandboxProxy> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.xspaceagi.sandbox.infra.dao.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.xspaceagi.sandbox.infra.dao.entity.SandboxConfig;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 沙盒配置服务接口
|
||||
*/
|
||||
public interface SandboxConfigService extends IService<SandboxConfig> {
|
||||
|
||||
/**
|
||||
* 根据用户ID查询配置列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 配置列表
|
||||
*/
|
||||
List<SandboxConfig> queryUserConfigsByType(Long userId);
|
||||
|
||||
/**
|
||||
* 查询全局配置列表
|
||||
*
|
||||
* @return 配置列表
|
||||
*/
|
||||
List<SandboxConfig> queryGlobalConfigs(Boolean isActive);
|
||||
|
||||
/**
|
||||
* 根据配置键查询用户配置
|
||||
*
|
||||
* @param configKey 配置键
|
||||
* @return 配置信息
|
||||
*/
|
||||
SandboxConfig queryUserConfigByKey(String configKey);
|
||||
|
||||
/**
|
||||
* 根据配置键查询全局配置
|
||||
*
|
||||
* @param configKey 配置键
|
||||
* @return 配置信息
|
||||
*/
|
||||
SandboxConfig queryGlobalConfigByKey(String configKey);
|
||||
|
||||
SandboxConfig querySandboxConfigById(Long sandboxId);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.xspaceagi.sandbox.infra.dao.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.xspaceagi.sandbox.infra.dao.entity.SandboxProxy;
|
||||
import com.xspaceagi.sandbox.infra.dao.vo.SandboxProxyBackend;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 临时代理服务接口
|
||||
*/
|
||||
public interface SandboxProxyService extends IService<SandboxProxy> {
|
||||
|
||||
/**
|
||||
* 根据代理键查询代理配置
|
||||
*
|
||||
* @param proxyKey 代理键
|
||||
* @return 代理配置
|
||||
*/
|
||||
SandboxProxy getByProxyKey(String proxyKey);
|
||||
|
||||
/**
|
||||
* 根据沙盒ID查询代理配置
|
||||
*
|
||||
* @param sandboxId 沙盒ID
|
||||
* @return 代理配置
|
||||
*/
|
||||
SandboxProxy getBySandboxId(Long sandboxId);
|
||||
|
||||
SandboxProxyBackend getBackendByProxyKey(String proxyKey);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询代理配置列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 代理配置列表
|
||||
*/
|
||||
java.util.List<SandboxProxy> listByUserId(Long userId);
|
||||
|
||||
|
||||
void removeByProxyKey(Long userId, String proxyKey);
|
||||
|
||||
List<SandboxProxy> listByUserIdAndSandboxId(Long userId, Long sandboxId);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.xspaceagi.sandbox.infra.dao.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.xspaceagi.sandbox.infra.dao.entity.SandboxConfig;
|
||||
import com.xspaceagi.sandbox.infra.dao.mapper.SandboxConfigMapper;
|
||||
import com.xspaceagi.sandbox.infra.dao.service.SandboxConfigService;
|
||||
import com.xspaceagi.sandbox.spec.enums.SandboxScopeEnum;
|
||||
import com.xspaceagi.system.spec.tenant.thread.TenantFunctions;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 沙盒配置服务实现
|
||||
*/
|
||||
@Service
|
||||
public class SandboxConfigServiceImpl extends ServiceImpl<SandboxConfigMapper, SandboxConfig> implements SandboxConfigService {
|
||||
|
||||
@Override
|
||||
public List<SandboxConfig> queryUserConfigsByType(Long userId) {
|
||||
LambdaQueryWrapper<SandboxConfig> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SandboxConfig::getScope, SandboxScopeEnum.USER)
|
||||
.eq(SandboxConfig::getUserId, userId)
|
||||
.orderByDesc(SandboxConfig::getId);
|
||||
|
||||
return list(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SandboxConfig> queryGlobalConfigs(Boolean isActive) {
|
||||
LambdaQueryWrapper<SandboxConfig> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SandboxConfig::getScope, SandboxScopeEnum.GLOBAL)
|
||||
.eq(isActive != null, SandboxConfig::getIsActive, isActive)
|
||||
.orderByAsc(SandboxConfig::getId);
|
||||
|
||||
return list(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SandboxConfig queryUserConfigByKey(String configKey) {
|
||||
LambdaQueryWrapper<SandboxConfig> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SandboxConfig::getScope, SandboxScopeEnum.USER)
|
||||
.eq(SandboxConfig::getConfigKey, configKey)
|
||||
.eq(SandboxConfig::getIsActive, true)
|
||||
.last("LIMIT 1");
|
||||
|
||||
return TenantFunctions.callWithIgnoreCheck(() -> getOne(queryWrapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public SandboxConfig queryGlobalConfigByKey(String configKey) {
|
||||
LambdaQueryWrapper<SandboxConfig> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SandboxConfig::getScope, SandboxScopeEnum.GLOBAL)
|
||||
.eq(SandboxConfig::getConfigKey, configKey)
|
||||
.eq(SandboxConfig::getIsActive, true)
|
||||
.last("LIMIT 1");
|
||||
|
||||
return getOne(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SandboxConfig querySandboxConfigById(Long sandboxId) {
|
||||
LambdaQueryWrapper<SandboxConfig> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SandboxConfig::getId, sandboxId)
|
||||
.eq(SandboxConfig::getIsActive, true)
|
||||
.last("LIMIT 1");
|
||||
return getOne(queryWrapper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.xspaceagi.sandbox.infra.dao.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.xspaceagi.sandbox.infra.dao.entity.SandboxConfig;
|
||||
import com.xspaceagi.sandbox.infra.dao.entity.SandboxProxy;
|
||||
import com.xspaceagi.sandbox.infra.dao.mapper.SandboxProxyMapper;
|
||||
import com.xspaceagi.sandbox.infra.dao.service.SandboxConfigService;
|
||||
import com.xspaceagi.sandbox.infra.dao.service.SandboxProxyService;
|
||||
import com.xspaceagi.sandbox.infra.dao.vo.SandboxProxyBackend;
|
||||
import com.xspaceagi.system.spec.cache.SimpleJvmHashCache;
|
||||
import com.xspaceagi.system.spec.tenant.thread.TenantFunctions;
|
||||
import com.xspaceagi.system.spec.utils.RedisUtil;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 临时代理服务实现
|
||||
*/
|
||||
@Service
|
||||
public class SandboxProxyServiceImpl extends ServiceImpl<SandboxProxyMapper, SandboxProxy> implements SandboxProxyService {
|
||||
|
||||
@Resource
|
||||
private SandboxConfigService sandboxConfigService;
|
||||
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
@Override
|
||||
public SandboxProxy getByProxyKey(String proxyKey) {
|
||||
LambdaQueryWrapper<SandboxProxy> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SandboxProxy::getProxyKey, proxyKey)
|
||||
.last("LIMIT 1");
|
||||
return getOne(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SandboxProxy getBySandboxId(Long sandboxId) {
|
||||
LambdaQueryWrapper<SandboxProxy> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SandboxProxy::getSandboxId, sandboxId)
|
||||
.last("LIMIT 1");
|
||||
return getOne(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SandboxProxyBackend getBackendByProxyKey(String proxyKey) {
|
||||
Object val = SimpleJvmHashCache.getHash(SandboxProxy.class.getName(), proxyKey);
|
||||
if (val != null) {
|
||||
return (SandboxProxyBackend) val;
|
||||
}
|
||||
SandboxProxyBackend backend = TenantFunctions.callWithIgnoreCheck(() -> {
|
||||
SandboxProxy byProxyKey = getByProxyKey(proxyKey);
|
||||
if (byProxyKey == null) {
|
||||
return null;
|
||||
}
|
||||
SandboxConfig sandboxConfig = sandboxConfigService.querySandboxConfigById(byProxyKey.getSandboxId());
|
||||
if (sandboxConfig == null) {
|
||||
return null;
|
||||
}
|
||||
return SandboxProxyBackend.builder()
|
||||
.sandboxId(byProxyKey.getSandboxId())
|
||||
.proxyKey(byProxyKey.getProxyKey())
|
||||
.backendHost(byProxyKey.getBackendHost())
|
||||
.backendPort(byProxyKey.getBackendPort())
|
||||
.sandboxConfigKey(sandboxConfig.getConfigKey())
|
||||
.build();
|
||||
});
|
||||
SimpleJvmHashCache.putHash(SandboxProxy.class.getName(), proxyKey, backend, 10);
|
||||
return backend;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SandboxProxy> listByUserId(Long userId) {
|
||||
LambdaQueryWrapper<SandboxProxy> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SandboxProxy::getUserId, userId)
|
||||
.orderByDesc(SandboxProxy::getCreated);
|
||||
return list(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeByProxyKey(Long userId, String proxyKey) {
|
||||
LambdaQueryWrapper<SandboxProxy> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SandboxProxy::getUserId, userId)
|
||||
.eq(SandboxProxy::getProxyKey, proxyKey);
|
||||
remove(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SandboxProxy> listByUserIdAndSandboxId(Long userId, Long sandboxId) {
|
||||
LambdaQueryWrapper<SandboxProxy> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SandboxProxy::getUserId, userId)
|
||||
.eq(SandboxProxy::getSandboxId, sandboxId)
|
||||
.orderByDesc(SandboxProxy::getCreated);
|
||||
return list(queryWrapper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.xspaceagi.sandbox.infra.dao.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 沙盒配置值
|
||||
*/
|
||||
@Schema(description = "沙盒配置值")
|
||||
@Data
|
||||
public class SandboxConfigValue {
|
||||
|
||||
@Schema(description = "服务根地址,例如 http://192.168.1.11,不允许携带端口")
|
||||
private String hostWithScheme;
|
||||
|
||||
@Schema(description = "Agent服务端口")
|
||||
private int agentPort;
|
||||
|
||||
@Schema(description = "VNC服务端口")
|
||||
private int vncPort;
|
||||
|
||||
@Schema(description = "文件服务端口")
|
||||
private int fileServerPort;
|
||||
|
||||
@Schema(description = "API密钥")
|
||||
private String apiKey;
|
||||
|
||||
@Schema(description = "最大用户数")
|
||||
private Integer maxUsers;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.xspaceagi.sandbox.infra.dao.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 临时代理实体类
|
||||
*/
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public class SandboxProxyBackend implements Serializable {
|
||||
|
||||
private Long sandboxId;
|
||||
|
||||
private String proxyKey;
|
||||
|
||||
private String backendHost;
|
||||
|
||||
private Integer backendPort;
|
||||
|
||||
private String sandboxConfigKey;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.xspaceagi.sandbox.infra.dao.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 沙盒配置值
|
||||
*/
|
||||
@Schema(description = "沙盒服务端内部交互信息")
|
||||
@Data
|
||||
public class SandboxServerInfo {
|
||||
|
||||
@Schema(description = "协议")
|
||||
private String scheme;
|
||||
|
||||
@Schema(description = "主机地址")
|
||||
private String host;
|
||||
|
||||
@Schema(description = "Agent服务端口")
|
||||
private int agentPort;
|
||||
|
||||
@Schema(description = "VNC服务端口")
|
||||
private int vncPort;
|
||||
|
||||
@Schema(description = "文件服务端口")
|
||||
private int fileServerPort;
|
||||
|
||||
@Schema(description = "API密钥")
|
||||
private String apiKey;
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
package com.xspaceagi.sandbox.infra.network;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* 端口池管理器
|
||||
*
|
||||
* 功能:
|
||||
* 1. 管理指定范围的端口池
|
||||
* 2. 支持端口借用和归还
|
||||
* 3. 归还的端口需要等待冷却时间(默认1分钟)后才能再次被使用
|
||||
* 4. 线程安全
|
||||
*/
|
||||
@Slf4j
|
||||
public class PortPoolManager {
|
||||
|
||||
/**
|
||||
* 端口信息
|
||||
*/
|
||||
private static class PortInfo {
|
||||
private final int port;
|
||||
private long allocateTime; // 分配时间
|
||||
private long releaseTime; // 释放时间(用于冷却)
|
||||
private boolean inUse; // 是否在使用中
|
||||
private boolean cooling; // 是否在冷却中
|
||||
|
||||
public PortInfo(int port) {
|
||||
this.port = port;
|
||||
this.inUse = false;
|
||||
this.cooling = false;
|
||||
}
|
||||
|
||||
public void allocate() {
|
||||
this.inUse = true;
|
||||
this.allocateTime = System.currentTimeMillis();
|
||||
this.releaseTime = 0;
|
||||
}
|
||||
|
||||
public void release() {
|
||||
this.inUse = false;
|
||||
this.cooling = true;
|
||||
this.releaseTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public boolean isAvailable(long coolDownMillis) {
|
||||
if (inUse) {
|
||||
return false;
|
||||
}
|
||||
if (cooling) {
|
||||
return System.currentTimeMillis() - releaseTime >= coolDownMillis;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
this.inUse = false;
|
||||
this.cooling = false;
|
||||
this.releaseTime = 0;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public long getAllocateTime() {
|
||||
return allocateTime;
|
||||
}
|
||||
|
||||
public long getReleaseTime() {
|
||||
return releaseTime;
|
||||
}
|
||||
}
|
||||
|
||||
private final int minPort;
|
||||
private final int maxPort;
|
||||
private final long coolDownMillis; // 冷却时间(毫秒)
|
||||
|
||||
// 端口信息映射
|
||||
private final Map<Integer, PortInfo> portInfoMap;
|
||||
|
||||
// 可用端口队列(线程安全)
|
||||
private final BlockingQueue<PortInfo> availablePorts;
|
||||
|
||||
// 定时检查冷却端口
|
||||
private final ScheduledExecutorService scheduler;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param minPort 起始端口(包含)
|
||||
* @param maxPort 结束端口(包含)
|
||||
* @param coolDownSeconds 冷却时间(秒)
|
||||
*/
|
||||
public PortPoolManager(int minPort, int maxPort, int coolDownSeconds) {
|
||||
if (minPort <= 0 || maxPort <= 0 || minPort > maxPort) {
|
||||
throw new IllegalArgumentException("Invalid port range");
|
||||
}
|
||||
if (coolDownSeconds <= 0) {
|
||||
throw new IllegalArgumentException("Cool down time must be positive");
|
||||
}
|
||||
|
||||
this.minPort = minPort;
|
||||
this.maxPort = maxPort;
|
||||
this.coolDownMillis = coolDownSeconds * 1000L;
|
||||
|
||||
this.portInfoMap = new ConcurrentHashMap<>();
|
||||
this.availablePorts = new LinkedBlockingQueue<>();
|
||||
this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread thread = new Thread(r, "port-pool-cooler");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
|
||||
initializePortPool();
|
||||
startCoolDownChecker();
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认构造函数(1分钟冷却时间)
|
||||
*
|
||||
* @param minPort 起始端口(包含)
|
||||
* @param maxPort 结束端口(包含)
|
||||
*/
|
||||
public PortPoolManager(int minPort, int maxPort) {
|
||||
this(minPort, maxPort, 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化端口池
|
||||
*/
|
||||
private void initializePortPool() {
|
||||
log.info("Initializing port pool: {} -> {}, cool down: {}ms", minPort, maxPort, coolDownMillis);
|
||||
|
||||
for (int port = minPort; port <= maxPort; port++) {
|
||||
PortInfo portInfo = new PortInfo(port);
|
||||
portInfoMap.put(port, portInfo);
|
||||
availablePorts.offer(portInfo);
|
||||
}
|
||||
|
||||
log.info("Port pool initialized with {} ports", maxPort - minPort + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动冷却端口检查器
|
||||
*/
|
||||
private void startCoolDownChecker() {
|
||||
// 每10秒检查一次冷却端口
|
||||
scheduler.scheduleAtFixedRate(() -> {
|
||||
try {
|
||||
checkAndReturnCoolingPorts();
|
||||
} catch (Exception e) {
|
||||
log.error("Error checking cooling ports", e);
|
||||
}
|
||||
}, 10, 10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并返回已冷却的端口
|
||||
*/
|
||||
private void checkAndReturnCoolingPorts() {
|
||||
int returnedCount = 0;
|
||||
|
||||
for (PortInfo portInfo : portInfoMap.values()) {
|
||||
if (portInfo.cooling && portInfo.isAvailable(0)) {
|
||||
// 冷却完成,重置状态并放回可用队列
|
||||
portInfo.reset();
|
||||
if (availablePorts.offer(portInfo)) {
|
||||
returnedCount++;
|
||||
log.debug("Port {} returned to pool after cooling", portInfo.getPort());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (returnedCount > 0) {
|
||||
log.info("Returned {} ports to pool after cooling", returnedCount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 借用端口(阻塞等待)
|
||||
*
|
||||
* @return 可用的端口
|
||||
* @throws InterruptedException 如果等待被中断
|
||||
*/
|
||||
public int borrow() throws Exception {
|
||||
return borrow(0, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 借用端口(带超时)
|
||||
*
|
||||
* @param timeout 超时时间
|
||||
* @param unit 时间单位
|
||||
* @return 可用的端口
|
||||
* @throws InterruptedException 如果等待被中断
|
||||
*/
|
||||
public int borrow(long timeout, TimeUnit unit) throws Exception {
|
||||
PortInfo portInfo;
|
||||
if (timeout > 0) {
|
||||
portInfo = availablePorts.poll(timeout, unit);
|
||||
} else {
|
||||
portInfo = availablePorts.take();
|
||||
}
|
||||
|
||||
if (portInfo == null) {
|
||||
throw new TimeoutException("No available ports in pool");
|
||||
}
|
||||
|
||||
portInfo.allocate();
|
||||
log.debug("Port {} borrowed", portInfo.getPort());
|
||||
|
||||
return portInfo.getPort();
|
||||
}
|
||||
|
||||
/**
|
||||
* 归还端口
|
||||
*
|
||||
* @param port 要归还的端口
|
||||
* @throws IllegalArgumentException 如果端口不在池中
|
||||
*/
|
||||
public void release(int port) {
|
||||
PortInfo portInfo = portInfoMap.get(port);
|
||||
if (portInfo == null) {
|
||||
throw new IllegalArgumentException("Port " + port + " is not managed by this pool");
|
||||
}
|
||||
|
||||
if (!portInfo.inUse) {
|
||||
log.warn("Port {} is not in use, ignore release", port);
|
||||
return;
|
||||
}
|
||||
|
||||
portInfo.release();
|
||||
log.debug("Port {} released, entering cooling period", port);
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制归还端口(不进入冷却期)
|
||||
*
|
||||
* @param port 要归还的端口
|
||||
*/
|
||||
public void releaseImmediately(int port) {
|
||||
PortInfo portInfo = portInfoMap.get(port);
|
||||
if (portInfo == null) {
|
||||
throw new IllegalArgumentException("Port " + port + " is not managed by this pool");
|
||||
}
|
||||
|
||||
boolean wasInUse = portInfo.inUse;
|
||||
portInfo.reset();
|
||||
|
||||
if (wasInUse) {
|
||||
// 如果之前在使用中,放回可用队列
|
||||
availablePorts.offer(portInfo);
|
||||
log.debug("Port {} released immediately (no cooling)", port);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取池中的端口总数
|
||||
*
|
||||
* @return 端口总数
|
||||
*/
|
||||
public int getTotalPorts() {
|
||||
return portInfoMap.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用端口数
|
||||
*
|
||||
* @return 可用端口数
|
||||
*/
|
||||
public int getAvailablePortCount() {
|
||||
return availablePorts.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取使用中的端口数
|
||||
*
|
||||
* @return 使用中的端口数
|
||||
*/
|
||||
public int getInUsePortCount() {
|
||||
int count = 0;
|
||||
for (PortInfo portInfo : portInfoMap.values()) {
|
||||
if (portInfo.inUse) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取冷却中的端口数
|
||||
*
|
||||
* @return 冷却中的端口数
|
||||
*/
|
||||
public int getCoolingPortCount() {
|
||||
int count = 0;
|
||||
for (PortInfo portInfo : portInfoMap.values()) {
|
||||
if (portInfo.cooling && !portInfo.inUse) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查端口是否在池中
|
||||
*
|
||||
* @param port 端口
|
||||
* @return 是否在池中
|
||||
*/
|
||||
public boolean containsPort(int port) {
|
||||
return portInfoMap.containsKey(port);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查端口是否可用
|
||||
*
|
||||
* @param port 端口
|
||||
* @return 是否可用
|
||||
*/
|
||||
public boolean isAvailable(int port) {
|
||||
PortInfo portInfo = portInfoMap.get(port);
|
||||
return portInfo != null && portInfo.isAvailable(coolDownMillis);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取端口状态
|
||||
*
|
||||
* @param port 端口
|
||||
* @return 状态字符串:AVAILABLE, IN_USE, COOLING, NOT_FOUND
|
||||
*/
|
||||
public String getPortStatus(int port) {
|
||||
PortInfo portInfo = portInfoMap.get(port);
|
||||
if (portInfo == null) {
|
||||
return "NOT_FOUND";
|
||||
}
|
||||
if (portInfo.inUse) {
|
||||
return "IN_USE";
|
||||
}
|
||||
if (portInfo.cooling) {
|
||||
return "COOLING";
|
||||
}
|
||||
return "AVAILABLE";
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭端口池
|
||||
*/
|
||||
public void shutdown() {
|
||||
log.info("Shutting down port pool manager");
|
||||
scheduler.shutdown();
|
||||
try {
|
||||
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
scheduler.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
scheduler.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
log.info("Port pool manager shutdown complete");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取端口池统计信息
|
||||
*
|
||||
* @return 统计信息
|
||||
*/
|
||||
public PortPoolStats getStats() {
|
||||
int inUse = getInUsePortCount();
|
||||
int available = getAvailablePortCount();
|
||||
int cooling = getCoolingPortCount();
|
||||
|
||||
return new PortPoolStats(
|
||||
minPort,
|
||||
maxPort,
|
||||
getTotalPorts(),
|
||||
available,
|
||||
inUse,
|
||||
cooling,
|
||||
coolDownMillis / 1000
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 端口池统计信息
|
||||
*/
|
||||
public record PortPoolStats(
|
||||
int minPort,
|
||||
int maxPort,
|
||||
int totalPorts,
|
||||
int availablePorts,
|
||||
int inUsePorts,
|
||||
int coolingPorts,
|
||||
long coolDownSeconds
|
||||
) {
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"PortPoolStats{range=%d-%d, total=%d, available=%d, inUse=%d, cooling=%d, coolDown=%ds}",
|
||||
minPort, maxPort, totalPorts, availablePorts, inUsePorts, coolingPorts, coolDownSeconds
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package com.xspaceagi.sandbox.infra.network;
|
||||
|
||||
import com.xspaceagi.sandbox.infra.network.protocol.Constants;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.util.AttributeKey;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 代理服务连接管理(代理客户端连接+用户请求连接)
|
||||
*
|
||||
* @author fengfei
|
||||
*/
|
||||
public class ProxyChannelManager {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ProxyChannelManager.class);
|
||||
|
||||
public static final AttributeKey<Map<String, Channel>> USER_CHANNELS = AttributeKey.newInstance("user_channels");
|
||||
|
||||
private static final AttributeKey<String> REQUEST_LAN_INFO = AttributeKey.newInstance("request_lan_info");
|
||||
|
||||
public static final AttributeKey<List<Integer>> CHANNEL_PORT = AttributeKey.newInstance("channel_port");
|
||||
|
||||
public static final AttributeKey<String> CHANNEL_CLIENT_KEY = AttributeKey.newInstance("channel_client_key");
|
||||
|
||||
private static final Map<Integer, Channel> portCmdChannelMapping = new ConcurrentHashMap<Integer, Channel>();
|
||||
|
||||
private static final Map<String, Channel> cmdChannels = new ConcurrentHashMap<String, Channel>();
|
||||
|
||||
private static final Map<Integer, LanInfoHolder> portLanInfoMapping = new ConcurrentHashMap<>();
|
||||
|
||||
public static void addCmdChannel(List<Integer> ports, String clientKey, Channel channel) {
|
||||
|
||||
if (ports == null) {
|
||||
throw new IllegalArgumentException("port can not be null");
|
||||
}
|
||||
|
||||
// 客户端(proxy-client)相对较少,这里同步的比较重
|
||||
// 保证服务器对外端口与客户端到服务器的连接关系在临界情况时调用removeChannel(Channel channel)时不出问题
|
||||
synchronized (portCmdChannelMapping) {
|
||||
for (int port : ports) {
|
||||
portCmdChannelMapping.put(port, channel);
|
||||
}
|
||||
}
|
||||
|
||||
channel.attr(CHANNEL_PORT).set(ports);
|
||||
channel.attr(CHANNEL_CLIENT_KEY).set(clientKey);
|
||||
channel.attr(USER_CHANNELS).set(new ConcurrentHashMap<String, Channel>());
|
||||
cmdChannels.put(clientKey, channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理客户端连接断开后清除关系
|
||||
*
|
||||
* @param channel
|
||||
*/
|
||||
public static void removeCmdChannel(Channel channel) {
|
||||
logger.info("channel closed, clear user channels, {}", channel);
|
||||
if (channel.attr(CHANNEL_PORT).get() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String clientKey = channel.attr(CHANNEL_CLIENT_KEY).get();
|
||||
Channel channel0 = cmdChannels.remove(clientKey);
|
||||
logger.info("remove cmd channel, {}", channel0);
|
||||
|
||||
List<Integer> ports = channel.attr(CHANNEL_PORT).get();
|
||||
for (int port : ports) {
|
||||
Channel proxyChannel = portCmdChannelMapping.remove(port);
|
||||
if (proxyChannel == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 在执行断连之前新的连接已经连上来了
|
||||
if (!proxyChannel.id().equals(channel.id())) {
|
||||
portCmdChannelMapping.put(port, proxyChannel);
|
||||
}
|
||||
}
|
||||
|
||||
if (channel.isActive()) {
|
||||
logger.info("disconnect proxy channel {}", channel);
|
||||
channel.close();
|
||||
}
|
||||
|
||||
Map<String, Channel> userChannels = getUserChannels(channel);
|
||||
for (String s : new ArrayList<>(userChannels.keySet())) {
|
||||
Channel userChannel = userChannels.get(s);
|
||||
if (userChannel.isActive()) {
|
||||
userChannel.close();
|
||||
logger.info("disconnect user channel {}", userChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Channel getCmdChannel(Integer port) {
|
||||
return portCmdChannelMapping.get(port);
|
||||
}
|
||||
|
||||
public static Channel getCmdChannel(String clientKey) {
|
||||
return cmdChannels.get(clientKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加用户连接与代理客户端连接关系
|
||||
*
|
||||
* @param cmdChannel
|
||||
* @param userId
|
||||
* @param userChannel
|
||||
*/
|
||||
public static void addUserChannelToCmdChannel(Channel cmdChannel, String userId, Channel userChannel) {
|
||||
InetSocketAddress sa = (InetSocketAddress) userChannel.localAddress();
|
||||
//String lanInfo = ProxyConfig.getInstance().getLanInfo(sa.getPort());
|
||||
userChannel.attr(Constants.USER_ID).set(userId);
|
||||
//userChannel.attr(REQUEST_LAN_INFO).set(lanInfo);
|
||||
cmdChannel.attr(USER_CHANNELS).get().put(userId, userChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户连接与代理客户端连接关系
|
||||
*
|
||||
* @param cmdChannel
|
||||
* @param userId
|
||||
* @return
|
||||
*/
|
||||
public static Channel removeUserChannelFromCmdChannel(Channel cmdChannel, String userId) {
|
||||
if (cmdChannel.attr(USER_CHANNELS).get() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
synchronized (cmdChannel) {
|
||||
return cmdChannel.attr(USER_CHANNELS).get().remove(userId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据代理客户端连接与用户编号获取用户连接
|
||||
*
|
||||
* @param cmdChannel
|
||||
* @param userId
|
||||
* @return
|
||||
*/
|
||||
public static Channel getUserChannel(Channel cmdChannel, String userId) {
|
||||
return cmdChannel.attr(USER_CHANNELS).get().get(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户编号
|
||||
*
|
||||
* @param userChannel
|
||||
* @return
|
||||
*/
|
||||
public static String getUserChannelUserId(Channel userChannel) {
|
||||
return userChannel.attr(Constants.USER_ID).get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户请求的内网IP端口信息
|
||||
*
|
||||
* @param userChannel
|
||||
* @return
|
||||
*/
|
||||
public static String getUserChannelRequestLanInfo(Channel userChannel) {
|
||||
return userChannel.attr(REQUEST_LAN_INFO).get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代理控制客户端连接绑定的所有用户连接
|
||||
*
|
||||
* @param cmdChannel
|
||||
* @return
|
||||
*/
|
||||
public static Map<String, Channel> getUserChannels(Channel cmdChannel) {
|
||||
return cmdChannel.attr(USER_CHANNELS).get();
|
||||
}
|
||||
|
||||
public static void addPortLanInfo(int port, String lanInfo, boolean isVnc) {
|
||||
portLanInfoMapping.put(port, new LanInfoHolder(lanInfo, isVnc));
|
||||
}
|
||||
|
||||
public static String getPortLanInfo(int port) {
|
||||
LanInfoHolder lanInfoHolder = portLanInfoMapping.get(port);
|
||||
return lanInfoHolder != null ? lanInfoHolder.getLanInfo() : null;
|
||||
}
|
||||
|
||||
public static boolean isVncPort(int port) {
|
||||
LanInfoHolder lanInfoHolder = portLanInfoMapping.get(port);
|
||||
return lanInfoHolder != null && lanInfoHolder.isVnc();
|
||||
}
|
||||
|
||||
public static void removePortLanInfo(int port) {
|
||||
portLanInfoMapping.remove(port);
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
private static class LanInfoHolder {
|
||||
private String lanInfo;
|
||||
private boolean isVnc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.xspaceagi.sandbox.infra.network;
|
||||
|
||||
import com.xspaceagi.sandbox.infra.dao.service.SandboxProxyService;
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import io.netty.handler.codec.http.HttpServerCodec;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 模型代理服务器
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ReverseHttpProxyServer {
|
||||
|
||||
@Value("${sandbox.temp-link.proxy.port:18087}")
|
||||
private int port;
|
||||
|
||||
@Resource
|
||||
private SandboxProxyService sandboxProxyService;
|
||||
|
||||
private EventLoopGroup bossGroup;
|
||||
private EventLoopGroup workerGroup;
|
||||
private Channel serverChannel;
|
||||
|
||||
@PostConstruct
|
||||
public void start() throws Exception {
|
||||
|
||||
bossGroup = new NioEventLoopGroup(1);
|
||||
workerGroup = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors() * 2);
|
||||
|
||||
try {
|
||||
ServerBootstrap b = new ServerBootstrap();
|
||||
b.group(bossGroup, workerGroup)
|
||||
.channel(NioServerSocketChannel.class)
|
||||
.childHandler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel ch) {
|
||||
ChannelPipeline p = ch.pipeline();
|
||||
p.addLast("httpServerCodec", new HttpServerCodec());
|
||||
p.addLast(new TempLinkHttpProxyHandler(sandboxProxyService));
|
||||
}
|
||||
})
|
||||
.option(ChannelOption.SO_BACKLOG, 128)
|
||||
.childOption(ChannelOption.SO_KEEPALIVE, true);
|
||||
|
||||
ChannelFuture f = b.bind(port).sync();
|
||||
serverChannel = f.channel();
|
||||
|
||||
log.info("sandbox proxy server started on port {}", port);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to start sandbox proxy server", e);
|
||||
shutdown();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
if (serverChannel != null) {
|
||||
serverChannel.close();
|
||||
}
|
||||
if (bossGroup != null) {
|
||||
bossGroup.shutdownGracefully();
|
||||
}
|
||||
if (workerGroup != null) {
|
||||
workerGroup.shutdownGracefully();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package com.xspaceagi.sandbox.infra.network;
|
||||
|
||||
import com.xspaceagi.sandbox.infra.config.ReverseServerProperties;
|
||||
import com.xspaceagi.sandbox.infra.dao.service.SandboxConfigService;
|
||||
import com.xspaceagi.sandbox.infra.network.protocol.IdleCheckHandler;
|
||||
import com.xspaceagi.sandbox.infra.network.protocol.ProxyMessageDecoder;
|
||||
import com.xspaceagi.sandbox.infra.network.protocol.ProxyMessageEncoder;
|
||||
import com.xspaceagi.system.spec.utils.RedisUtil;
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import io.netty.handler.codec.http.HttpObjectAggregator;
|
||||
import io.netty.handler.codec.http.HttpServerCodec;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
|
||||
import io.netty.handler.ssl.SslHandler;
|
||||
import io.netty.handler.stream.ChunkedWriteHandler;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.Getter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import java.net.BindException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Component
|
||||
public class ReverseServerContainer {
|
||||
|
||||
/**
|
||||
* max packet is 2M.
|
||||
*/
|
||||
private static final int MAX_FRAME_LENGTH = 2 * 1024 * 1024;
|
||||
|
||||
private static final int LENGTH_FIELD_OFFSET = 0;
|
||||
|
||||
private static final int LENGTH_FIELD_LENGTH = 4;
|
||||
|
||||
private static final int INITIAL_BYTES_TO_STRIP = 0;
|
||||
|
||||
private static final int LENGTH_ADJUSTMENT = 0;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ReverseServerContainer.class);
|
||||
|
||||
private NioEventLoopGroup serverWorkerGroup;
|
||||
|
||||
private NioEventLoopGroup serverBossGroup;
|
||||
|
||||
private ServerBootstrap clientBootstrap;
|
||||
|
||||
private volatile boolean started = false;
|
||||
|
||||
@Resource
|
||||
private SandboxConfigService sandboxConfigService;
|
||||
|
||||
@Getter
|
||||
private PortPoolManager portPoolManager;
|
||||
|
||||
@Getter
|
||||
@Resource
|
||||
private ReverseServerProperties reverseServerProperties;
|
||||
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
private final Map<Integer, ChannelFuture> channelFutureMap = new ConcurrentHashMap<>();
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
synchronized (ReverseServerContainer.class) {
|
||||
if (!started) {
|
||||
start();
|
||||
started = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 最大HTTP内容长度 (8MB)
|
||||
*/
|
||||
private static final int MAX_HTTP_CONTENT_LENGTH = 8 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* 最大WebSocket帧长度 (10MB)
|
||||
*/
|
||||
private static final int MAX_FRAME_SIZE = 10 * 1024 * 1024;
|
||||
|
||||
public void start() {
|
||||
serverBossGroup = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors());
|
||||
serverWorkerGroup = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors() * 2);
|
||||
clientBootstrap = new ServerBootstrap();
|
||||
clientBootstrap.group(serverBossGroup, serverWorkerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
|
||||
|
||||
@Override
|
||||
public void initChannel(SocketChannel ch) {
|
||||
int port = ch.localAddress().getPort();
|
||||
if (ProxyChannelManager.isVncPort(port)) {
|
||||
// HTTP Codec - 解码HTTP请求
|
||||
ch.pipeline().addLast("httpCodec", new HttpServerCodec());
|
||||
|
||||
// HTTP Object Aggregator - 将HTTP消息聚合为完整请求
|
||||
ch.pipeline().addLast("httpAggregator", new HttpObjectAggregator(MAX_HTTP_CONTENT_LENGTH));
|
||||
|
||||
// Chunked Write Handler - 支持大文件传输
|
||||
ch.pipeline().addLast("chunkedWrite", new ChunkedWriteHandler());
|
||||
|
||||
// WebSocket Server Protocol Handler - 处理握手和协议升级
|
||||
ch.pipeline().addLast("wsProtocol", new WebSocketServerProtocolHandler(
|
||||
"/websockify",
|
||||
null, // subprotocols
|
||||
true, // allowExtensions
|
||||
MAX_FRAME_SIZE // maxFrameSize
|
||||
));
|
||||
|
||||
// WebSocket Frame to ByteBuf Handler - WebSocket与TCP转换层
|
||||
ch.pipeline().addLast("wsConverter", new WebSocketFrameToByteBufHandler());
|
||||
}
|
||||
ch.pipeline().addLast(new UserChannelHandler());
|
||||
}
|
||||
});
|
||||
|
||||
initializeSSLTCPTransport(new SslContextCreator().initSSLContext());
|
||||
portPoolManager = new PortPoolManager(reverseServerProperties.getInner().getMinPort(), reverseServerProperties.getInner().getMaxPort(), 30);
|
||||
}
|
||||
|
||||
private void initializeSSLTCPTransport(final SSLContext sslContext) {
|
||||
ServerBootstrap b = new ServerBootstrap();
|
||||
b.group(serverBossGroup, serverWorkerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
|
||||
|
||||
@Override
|
||||
public void initChannel(SocketChannel ch) throws Exception {
|
||||
ChannelPipeline pipeline = ch.pipeline();
|
||||
try {
|
||||
pipeline.addLast("ssl", createSslHandler(sslContext));
|
||||
ch.pipeline().addLast(new ProxyMessageDecoder(MAX_FRAME_LENGTH, LENGTH_FIELD_OFFSET, LENGTH_FIELD_LENGTH, LENGTH_ADJUSTMENT, INITIAL_BYTES_TO_STRIP));
|
||||
ch.pipeline().addLast(new ProxyMessageEncoder());
|
||||
ch.pipeline().addLast(new IdleCheckHandler(IdleCheckHandler.READ_IDLE_TIME, IdleCheckHandler.WRITE_IDLE_TIME, 0));
|
||||
ch.pipeline().addLast(new ServerChannelHandler(sandboxConfigService, ReverseServerContainer.this));
|
||||
} catch (Throwable th) {
|
||||
logger.error("Severe error during pipeline creation", th);
|
||||
throw th;
|
||||
}
|
||||
}
|
||||
});
|
||||
try {
|
||||
// Bind and start to accept incoming connections.
|
||||
b.bind("0.0.0.0", reverseServerProperties.getOuter().getPort()).get();
|
||||
logger.info("proxy ssl server start on port {}", reverseServerProperties.getOuter().getPort());
|
||||
} catch (Exception ex) {
|
||||
logger.error("An interruptedException was caught while initializing server", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean startUserPort(String host, Integer port) {
|
||||
Assert.notNull(port, "port must not be null");
|
||||
try {
|
||||
ChannelFuture future = clientBootstrap.bind(port);
|
||||
future.get();
|
||||
channelFutureMap.put(port, future);
|
||||
logger.info("bind user port {}", port);
|
||||
} catch (Exception ex) {
|
||||
// BindException表示该端口已经绑定过
|
||||
if (!(ex.getCause() instanceof BindException)) {
|
||||
logger.error("An interruptedException was caught while initializing server", ex);
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
logger.warn("bind port {} error", port, ex);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void releaseUserPort(Integer port) {
|
||||
ChannelFuture channelFuture = channelFutureMap.get(port);
|
||||
if (channelFuture != null) {
|
||||
channelFuture.channel().close();
|
||||
channelFutureMap.remove(port);
|
||||
logger.info("release user port {}", port);
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
serverBossGroup.shutdownGracefully();
|
||||
serverWorkerGroup.shutdownGracefully();
|
||||
}
|
||||
|
||||
private ChannelHandler createSslHandler(SSLContext sslContext) {
|
||||
SSLEngine sslEngine = sslContext.createSSLEngine();
|
||||
sslEngine.setUseClientMode(false);
|
||||
return new SslHandler(sslEngine);
|
||||
}
|
||||
|
||||
public void userSandboxOnlineTouch(String key) {
|
||||
redisUtil.set("user:sandbox:status:" + key, String.valueOf(System.currentTimeMillis()), 60);
|
||||
}
|
||||
|
||||
public Long getUserSandboxAliveTime(String key) {
|
||||
Object value = redisUtil.get("user:sandbox:status:" + key);
|
||||
if (value != null) {
|
||||
return Long.parseLong(value.toString());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void userSandboxOfflineTouch(String key) {
|
||||
redisUtil.expire("user:sandbox:status:" + key, 0);
|
||||
}
|
||||
|
||||
public void offlineClient(String key) {
|
||||
Channel cmdChannel = ProxyChannelManager.getCmdChannel(key);
|
||||
if (cmdChannel != null && cmdChannel.isActive()) {
|
||||
cmdChannel.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
package com.xspaceagi.sandbox.infra.network;
|
||||
|
||||
import cn.hutool.core.collection.ConcurrentHashSet;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.xspaceagi.sandbox.infra.dao.entity.SandboxConfig;
|
||||
import com.xspaceagi.sandbox.infra.dao.service.SandboxConfigService;
|
||||
import com.xspaceagi.sandbox.infra.dao.vo.SandboxConfigValue;
|
||||
import com.xspaceagi.sandbox.infra.dao.vo.SandboxServerInfo;
|
||||
import com.xspaceagi.sandbox.infra.network.protocol.Constants;
|
||||
import com.xspaceagi.sandbox.infra.network.protocol.ProxyMessage;
|
||||
import com.xspaceagi.system.spec.tenant.thread.TenantFunctions;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.xspaceagi.sandbox.infra.network.ProxyChannelManager.*;
|
||||
|
||||
|
||||
/**
|
||||
* @author fengfei
|
||||
*/
|
||||
public class ServerChannelHandler extends SimpleChannelInboundHandler<ProxyMessage> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ServerChannelHandler.class);
|
||||
|
||||
private static final Set<String> authLockSet = new ConcurrentHashSet<>();
|
||||
|
||||
private final SandboxConfigService sandboxConfigService;
|
||||
|
||||
private final ReverseServerContainer reverseServerContainer;
|
||||
|
||||
public ServerChannelHandler(SandboxConfigService sandboxConfigService, ReverseServerContainer reverseServerContainer) {
|
||||
this.sandboxConfigService = sandboxConfigService;
|
||||
this.reverseServerContainer = reverseServerContainer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, ProxyMessage proxyMessage) throws Exception {
|
||||
logger.debug("ProxyMessage received {}", proxyMessage.getType());
|
||||
switch (proxyMessage.getType()) {
|
||||
case ProxyMessage.TYPE_HEARTBEAT:
|
||||
handleHeartbeatMessage(ctx, proxyMessage);
|
||||
break;
|
||||
case ProxyMessage.C_TYPE_AUTH:
|
||||
try {
|
||||
if (!authLockSet.add(proxyMessage.getUri())) {
|
||||
ctx.channel().close();
|
||||
return;
|
||||
}
|
||||
handleAuthMessage(ctx, proxyMessage);
|
||||
} finally {
|
||||
authLockSet.remove(proxyMessage.getUri());
|
||||
}
|
||||
break;
|
||||
case ProxyMessage.TYPE_CONNECT:
|
||||
handleConnectMessage(ctx, proxyMessage);
|
||||
break;
|
||||
case ProxyMessage.TYPE_DISCONNECT:
|
||||
handleDisconnectMessage(ctx, proxyMessage);
|
||||
break;
|
||||
case ProxyMessage.P_TYPE_TRANSFER:
|
||||
handleTransferMessage(ctx, proxyMessage);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTransferMessage(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
|
||||
Channel userChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
|
||||
if (userChannel != null) {
|
||||
ByteBuf buf = ctx.alloc().buffer(proxyMessage.getData().length);
|
||||
buf.writeBytes(proxyMessage.getData());
|
||||
userChannel.writeAndFlush(buf);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDisconnectMessage(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
|
||||
String clientKey = ctx.channel().attr(Constants.CLIENT_KEY).get();
|
||||
|
||||
// 代理连接没有连上服务器由控制连接发送用户端断开连接消息
|
||||
if (clientKey == null) {
|
||||
String userId = proxyMessage.getUri();
|
||||
Channel userChannel = ProxyChannelManager.removeUserChannelFromCmdChannel(ctx.channel(), userId);
|
||||
if (userChannel != null) {
|
||||
// 数据发送完成后再关闭连接,解决http1.0数据传输问题
|
||||
userChannel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Channel cmdChannel = ProxyChannelManager.getCmdChannel(clientKey);
|
||||
if (cmdChannel == null) {
|
||||
logger.warn("ConnectMessage:error cmd channel key {}", ctx.channel().attr(Constants.CLIENT_KEY).get());
|
||||
return;
|
||||
}
|
||||
|
||||
Channel userChannel = ProxyChannelManager.removeUserChannelFromCmdChannel(cmdChannel, ctx.channel().attr(Constants.USER_ID).get());
|
||||
if (userChannel != null) {
|
||||
// 数据发送完成后再关闭连接,解决http1.0数据传输问题
|
||||
userChannel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
|
||||
ctx.channel().attr(Constants.NEXT_CHANNEL).set(null);
|
||||
ctx.channel().attr(Constants.CLIENT_KEY).set(null);
|
||||
ctx.channel().attr(Constants.USER_ID).set(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleConnectMessage(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
|
||||
String uri = proxyMessage.getUri();
|
||||
if (uri == null) {
|
||||
ctx.channel().close();
|
||||
logger.warn("ConnectMessage:null uri");
|
||||
return;
|
||||
}
|
||||
|
||||
String[] tokens = uri.split("@");
|
||||
if (tokens.length != 2) {
|
||||
ctx.channel().close();
|
||||
logger.warn("ConnectMessage:error uri");
|
||||
return;
|
||||
}
|
||||
|
||||
Channel cmdChannel = ProxyChannelManager.getCmdChannel(tokens[1]);
|
||||
if (cmdChannel == null) {
|
||||
ctx.channel().close();
|
||||
logger.warn("ConnectMessage:error cmd channel key {}", tokens[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
Channel userChannel = ProxyChannelManager.getUserChannel(cmdChannel, tokens[0]);
|
||||
if (userChannel != null) {
|
||||
ctx.channel().attr(Constants.USER_ID).set(tokens[0]);
|
||||
ctx.channel().attr(Constants.CLIENT_KEY).set(tokens[1]);
|
||||
ctx.channel().attr(Constants.NEXT_CHANNEL).set(userChannel);
|
||||
userChannel.attr(Constants.NEXT_CHANNEL).set(ctx.channel());
|
||||
|
||||
// 读取http代理 TempLinkHttpProxyHandler 连接时的缓存数据
|
||||
Queue<Object> objects = userChannel.attr(Constants.MESSAGE_QUEUE).get();
|
||||
if (objects != null) {
|
||||
while (!objects.isEmpty()) {
|
||||
Object poll = objects.poll();
|
||||
if (poll instanceof ByteBuf buf) {
|
||||
byte[] bytes = new byte[buf.readableBytes()];
|
||||
buf.readBytes(bytes);
|
||||
String userId = ProxyChannelManager.getUserChannelUserId(userChannel);
|
||||
ProxyMessage proxyMessage0 = new ProxyMessage();
|
||||
proxyMessage0.setType(ProxyMessage.P_TYPE_TRANSFER);
|
||||
proxyMessage0.setUri(userId);
|
||||
proxyMessage0.setData(bytes);
|
||||
ctx.channel().writeAndFlush(proxyMessage0);
|
||||
}
|
||||
ReferenceCountUtil.release(poll);
|
||||
}
|
||||
}
|
||||
// 代理客户端与后端服务器连接成功,修改用户连接为可读状态
|
||||
userChannel.config().setOption(ChannelOption.AUTO_READ, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleHeartbeatMessage(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
|
||||
ProxyMessage heartbeatMessage = new ProxyMessage();
|
||||
heartbeatMessage.setSerialNumber(heartbeatMessage.getSerialNumber());
|
||||
heartbeatMessage.setType(ProxyMessage.TYPE_HEARTBEAT);
|
||||
logger.debug("response heartbeat message {}", ctx.channel());
|
||||
ctx.channel().writeAndFlush(heartbeatMessage);
|
||||
String clientKey = ctx.channel().attr(CHANNEL_CLIENT_KEY).get();
|
||||
Map<String, Channel> channelMap = ctx.channel().attr(USER_CHANNELS).get();
|
||||
if (clientKey != null && channelMap != null) {
|
||||
reverseServerContainer.userSandboxOnlineTouch(clientKey);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleAuthMessage(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
|
||||
String clientKey = proxyMessage.getUri();
|
||||
SandboxConfig sandboxConfig = sandboxConfigService.queryUserConfigByKey(clientKey);
|
||||
if (sandboxConfig == null) {
|
||||
logger.warn("error clientKey {}, {}", clientKey, ctx.channel());
|
||||
ctx.channel().close();
|
||||
return;
|
||||
}
|
||||
|
||||
Channel channel = ProxyChannelManager.getCmdChannel(clientKey);
|
||||
if (channel != null) {
|
||||
logger.warn("exist channel for key {}, {}", clientKey, channel);
|
||||
ctx.channel().close();
|
||||
channel.close();
|
||||
return;
|
||||
}
|
||||
SandboxConfigValue configValue = JSON.parseObject(sandboxConfig.getConfigValue(), SandboxConfigValue.class);
|
||||
if (configValue == null) {
|
||||
logger.warn("error configValue for key {}, {}", clientKey, ctx.channel());
|
||||
ctx.channel().close();
|
||||
return;
|
||||
}
|
||||
Integer agentInnerPort = null;
|
||||
Integer vncInnerPort = null;
|
||||
Integer fileServerPort = null;
|
||||
try {
|
||||
agentInnerPort = acquireUserPort();
|
||||
vncInnerPort = acquireUserPort();
|
||||
fileServerPort = acquireUserPort();
|
||||
|
||||
URL url;
|
||||
try {
|
||||
url = new URL(configValue.getHostWithScheme());
|
||||
} catch (MalformedURLException e) {
|
||||
logger.warn("error get url for key {}, {}", clientKey, ctx.channel(), e);
|
||||
ctx.channel().close();
|
||||
return;
|
||||
}
|
||||
|
||||
ProxyChannelManager.addPortLanInfo(agentInnerPort, url.getHost() + ":" + configValue.getAgentPort(), false);
|
||||
ProxyChannelManager.addPortLanInfo(vncInnerPort, url.getHost() + ":" + configValue.getVncPort(), false);//后续支持直连vnc
|
||||
ProxyChannelManager.addPortLanInfo(fileServerPort, url.getHost() + ":" + configValue.getFileServerPort(), false);
|
||||
|
||||
String innerHost = reverseServerContainer.getReverseServerProperties().getInner().getServiceHost();
|
||||
SandboxServerInfo sandboxServerInfo = new SandboxServerInfo();
|
||||
sandboxServerInfo.setScheme(url.getProtocol());
|
||||
sandboxServerInfo.setHost(innerHost);
|
||||
sandboxServerInfo.setAgentPort(agentInnerPort);
|
||||
sandboxServerInfo.setVncPort(vncInnerPort);
|
||||
sandboxServerInfo.setFileServerPort(fileServerPort);
|
||||
sandboxServerInfo.setApiKey(configValue.getApiKey());
|
||||
SandboxConfig sandboxConfigUpdate = new SandboxConfig();
|
||||
sandboxConfigUpdate.setId(sandboxConfig.getId());
|
||||
sandboxConfigUpdate.setServerInfo(JSON.toJSONString(sandboxServerInfo));
|
||||
|
||||
TenantFunctions.runWithIgnoreCheck(() -> sandboxConfigService.updateById(sandboxConfigUpdate));
|
||||
|
||||
List<Integer> ports = List.of(agentInnerPort, vncInnerPort, fileServerPort);
|
||||
logger.info("set port => channel, {}, {}, {}", clientKey, ports, ctx.channel());
|
||||
ProxyChannelManager.addCmdChannel(ports, clientKey, ctx.channel());
|
||||
reverseServerContainer.userSandboxOnlineTouch(clientKey);
|
||||
} catch (Exception e) {
|
||||
if (agentInnerPort != null) {
|
||||
reverseServerContainer.getPortPoolManager().release(agentInnerPort);
|
||||
}
|
||||
if (vncInnerPort != null) {
|
||||
reverseServerContainer.getPortPoolManager().release(vncInnerPort);
|
||||
}
|
||||
if (fileServerPort != null) {
|
||||
reverseServerContainer.getPortPoolManager().release(fileServerPort);
|
||||
}
|
||||
logger.warn("error get port for key {}, {}", clientKey, ctx.channel(), e);
|
||||
ctx.channel().close();
|
||||
}
|
||||
}
|
||||
|
||||
// acquire user port
|
||||
private Integer acquireUserPort() throws Exception {
|
||||
Integer port = null;
|
||||
int i = 0;
|
||||
while (i < 10) {
|
||||
port = reverseServerContainer.getPortPoolManager().borrow();
|
||||
boolean bind = reverseServerContainer.startUserPort(reverseServerContainer.getReverseServerProperties().getInner().getBindHost(), port);
|
||||
if (!bind) {
|
||||
logger.warn("port {} bind failed", port);
|
||||
reverseServerContainer.getPortPoolManager().release(port);
|
||||
port = null;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (port == null) {
|
||||
logger.warn("acquire port failed");
|
||||
throw new RuntimeException("acquire port failed");
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
|
||||
Channel userChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
|
||||
if (userChannel != null) {
|
||||
userChannel.config().setOption(ChannelOption.AUTO_READ, ctx.channel().isWritable());
|
||||
}
|
||||
|
||||
super.channelWritabilityChanged(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
||||
Channel userChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
|
||||
if (userChannel != null && userChannel.isActive()) {
|
||||
String clientKey = ctx.channel().attr(Constants.CLIENT_KEY).get();
|
||||
String userId = ctx.channel().attr(Constants.USER_ID).get();
|
||||
Channel cmdChannel = ProxyChannelManager.getCmdChannel(clientKey);
|
||||
if (cmdChannel != null) {
|
||||
ProxyChannelManager.removeUserChannelFromCmdChannel(cmdChannel, userId);
|
||||
} else {
|
||||
logger.warn("null cmdChannel, clientKey is {}", clientKey);
|
||||
}
|
||||
|
||||
// 数据发送完成后再关闭连接,解决http1.0数据传输问题
|
||||
userChannel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
|
||||
} else {
|
||||
Map<String, Channel> stringChannelMap = ctx.channel().attr(USER_CHANNELS).get();
|
||||
List<Integer> ports = ctx.channel().attr(CHANNEL_PORT).get();
|
||||
if (ports != null) {
|
||||
for (int port : ports) {
|
||||
ProxyChannelManager.removePortLanInfo(port);
|
||||
// 如果之前有连接在使用,避免客户端重试导致混乱,端口进入冷却期
|
||||
if (stringChannelMap != null && !stringChannelMap.isEmpty()) {
|
||||
reverseServerContainer.getPortPoolManager().release(port);
|
||||
} else {
|
||||
reverseServerContainer.getPortPoolManager().releaseImmediately(port);
|
||||
}
|
||||
reverseServerContainer.releaseUserPort(port);
|
||||
}
|
||||
}
|
||||
String clientKey = ctx.channel().attr(CHANNEL_CLIENT_KEY).get();
|
||||
if (clientKey != null) {
|
||||
reverseServerContainer.userSandboxOfflineTouch(clientKey);
|
||||
}
|
||||
ProxyChannelManager.removeCmdChannel(ctx.channel());
|
||||
}
|
||||
|
||||
super.channelInactive(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
logger.error("exception caught", cause);
|
||||
super.exceptionCaught(ctx, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.xspaceagi.sandbox.infra.network;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.security.*;
|
||||
import java.security.cert.CertificateException;
|
||||
|
||||
public class SslContextCreator {
|
||||
|
||||
private static Logger logger = LoggerFactory.getLogger(SslContextCreator.class);
|
||||
|
||||
public SSLContext initSSLContext() {
|
||||
logger.info("Checking SSL configuration properties...");
|
||||
final String jksPath = "ssl.jks";
|
||||
logger.info("Initializing SSL context. KeystorePath = {}.", jksPath);
|
||||
|
||||
// if we have the port also the jks then keyStorePassword and
|
||||
// keyManagerPassword
|
||||
// has to be defined
|
||||
final String keyStorePassword = "123456";
|
||||
final String keyManagerPassword = "123456";
|
||||
|
||||
// if client authentification is enabled a trustmanager needs to be
|
||||
// added to the ServerContext
|
||||
boolean needsClientAuth = false;
|
||||
|
||||
try {
|
||||
logger.info("Loading keystore. KeystorePath = {}.", jksPath);
|
||||
try (InputStream jksInputStream = jksDatastore(jksPath)) {
|
||||
if (jksInputStream == null) {
|
||||
logger.warn("The keystore input stream is null. The SSL context won't be initialized.");
|
||||
return null;
|
||||
}
|
||||
SSLContext serverContext = SSLContext.getInstance("TLS");
|
||||
final KeyStore ks = KeyStore.getInstance("JKS");
|
||||
ks.load(jksInputStream, keyStorePassword.toCharArray());
|
||||
logger.info("Initializing key manager...");
|
||||
final KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||
kmf.init(ks, keyManagerPassword.toCharArray());
|
||||
// init sslContext
|
||||
logger.info("Initializing SSL context...");
|
||||
serverContext.init(kmf.getKeyManagers(), null, null);
|
||||
logger.info("The SSL context has been initialized successfully.");
|
||||
|
||||
return serverContext;
|
||||
}
|
||||
} catch (NoSuchAlgorithmException | UnrecoverableKeyException | CertificateException | KeyStoreException
|
||||
| KeyManagementException | IOException ex) {
|
||||
logger.error("Unable to initialize SSL context. Cause = {}, errorMessage = {}.", ex.getCause(),
|
||||
ex.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream jksDatastore(String jksPath) throws FileNotFoundException {
|
||||
URL jksUrl = getClass().getClassLoader().getResource(jksPath);
|
||||
if (jksUrl != null) {
|
||||
logger.info("Starting with jks at {}, jks normal {}", jksUrl.toExternalForm(), jksUrl);
|
||||
return getClass().getClassLoader().getResourceAsStream(jksPath);
|
||||
}
|
||||
|
||||
logger.warn("No keystore has been found in the bundled resources. Scanning filesystem...");
|
||||
File jksFile = new File(jksPath);
|
||||
if (jksFile.exists()) {
|
||||
logger.info("Loading external keystore. Url = {}.", jksFile.getAbsolutePath());
|
||||
return new FileInputStream(jksFile);
|
||||
}
|
||||
|
||||
logger.warn("The keystore file does not exist. Url = {}.", jksFile.getAbsolutePath());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package com.xspaceagi.sandbox.infra.network;
|
||||
|
||||
import com.xspaceagi.sandbox.infra.dao.service.SandboxProxyService;
|
||||
import com.xspaceagi.sandbox.infra.dao.vo.SandboxProxyBackend;
|
||||
import com.xspaceagi.sandbox.infra.network.protocol.Constants;
|
||||
import com.xspaceagi.sandbox.infra.network.protocol.ProxyMessage;
|
||||
import com.xspaceagi.system.spec.cache.SimpleJvmHashCache;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufAllocator;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.handler.codec.http.*;
|
||||
import io.netty.util.CharsetUtil;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import java.net.InetAddress;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
|
||||
import static com.xspaceagi.sandbox.infra.network.UserChannelHandler.newUserId;
|
||||
|
||||
/**
|
||||
* 客户端临时代理
|
||||
*/
|
||||
@Slf4j
|
||||
public class TempLinkHttpProxyHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(TempLinkHttpProxyHandler.class);
|
||||
|
||||
private final SandboxProxyService sandboxProxyService;
|
||||
|
||||
private final Queue<Object> receivedLastMessagesWhenConnect = new LinkedList<>();
|
||||
|
||||
public TempLinkHttpProxyHandler(SandboxProxyService sandboxProxyService) {
|
||||
this.sandboxProxyService = sandboxProxyService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(final ChannelHandlerContext ctx, final Object msg) {
|
||||
if (msg instanceof HttpRequest) {
|
||||
DefaultHttpRequest request = (DefaultHttpRequest) msg;
|
||||
String host = request.headers().get("Host");
|
||||
//获取域名开头字符串
|
||||
String proxyKey = extractDomainPrefix(host);
|
||||
SandboxProxyBackend sandboxProxyBackend = proxyKey == null ? null : sandboxProxyService.getBackendByProxyKey(proxyKey);
|
||||
if (sandboxProxyBackend == null) {
|
||||
sendError(ctx, "Invalid domain", msg);
|
||||
return;
|
||||
}
|
||||
ctx.channel().pipeline().remove("httpServerCodec");
|
||||
Channel cmdChannel = ProxyChannelManager.getCmdChannel(sandboxProxyBackend.getSandboxConfigKey());
|
||||
if (cmdChannel == null) {
|
||||
log.warn("no cmd channel {}", sandboxProxyBackend);
|
||||
// 该端口还没有代理客户端
|
||||
sendError(ctx, "Claw is offline", msg);
|
||||
return;
|
||||
} else {
|
||||
String userId = newUserId();
|
||||
Channel userChannel = ctx.channel();
|
||||
// 用户连接到代理服务器时,设置用户连接不可读,等待代理后端服务器连接成功后再改变为可读状态
|
||||
userChannel.config().setOption(ChannelOption.AUTO_READ, false);
|
||||
ProxyChannelManager.addUserChannelToCmdChannel(cmdChannel, userId, userChannel);
|
||||
ProxyMessage proxyMessage = new ProxyMessage();
|
||||
proxyMessage.setType(ProxyMessage.TYPE_CONNECT);
|
||||
proxyMessage.setUri(userId);
|
||||
proxyMessage.setData((sandboxProxyBackend.getBackendHost() + ":" + sandboxProxyBackend.getBackendPort()).getBytes());
|
||||
cmdChannel.writeAndFlush(proxyMessage);
|
||||
}
|
||||
ByteBuf firstRequest = convertToByteBuf(request);
|
||||
receivedLastMessagesWhenConnect.offer(firstRequest);
|
||||
ctx.channel().attr(Constants.MESSAGE_QUEUE).set(receivedLastMessagesWhenConnect);
|
||||
ReferenceCountUtil.release(request);
|
||||
} else {
|
||||
Channel targetChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
|
||||
if (targetChannel != null) {
|
||||
if (msg instanceof ByteBuf buf) {
|
||||
byte[] bytes = new byte[buf.readableBytes()];
|
||||
buf.readBytes(bytes);
|
||||
String userId = ProxyChannelManager.getUserChannelUserId(ctx.channel());
|
||||
ProxyMessage proxyMessage0 = new ProxyMessage();
|
||||
proxyMessage0.setType(ProxyMessage.P_TYPE_TRANSFER);
|
||||
proxyMessage0.setUri(userId);
|
||||
proxyMessage0.setData(bytes);
|
||||
targetChannel.writeAndFlush(proxyMessage0);
|
||||
ReferenceCountUtil.release(buf);
|
||||
}
|
||||
} else {
|
||||
if (msg instanceof HttpContent) {
|
||||
ByteBuf content = ((HttpContent) msg).content();
|
||||
receivedLastMessagesWhenConnect.offer(content);
|
||||
} else if (msg instanceof ByteBuf) {
|
||||
receivedLastMessagesWhenConnect.offer(msg);
|
||||
} else {
|
||||
log.warn("unexpected message type {}", msg.getClass());
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sendError(ChannelHandlerContext ctx, String message, Object msg) {
|
||||
FullHttpResponse resp = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND,
|
||||
Unpooled.copiedBuffer("{\"error\":{\"message\":\"" + message + "\"}}", CharsetUtil.UTF_8));
|
||||
resp.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=UTF-8");
|
||||
ctx.channel().writeAndFlush(resp).addListener(ChannelFutureListener.CLOSE);
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
|
||||
Channel targetChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
|
||||
if (targetChannel != null) {
|
||||
targetChannel.config().setOption(ChannelOption.AUTO_READ, ctx.channel().isWritable());
|
||||
}
|
||||
super.channelWritabilityChanged(ctx);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void channelActive(ChannelHandlerContext ctx) throws Exception {
|
||||
super.channelActive(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
||||
Channel targetChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
|
||||
if (targetChannel != null) {
|
||||
try {
|
||||
targetChannel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
super.channelInactive(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
if (cause.getCause() != null && (cause.getCause() instanceof SSLHandshakeException)) {
|
||||
logger.warn("SSLHandshakeException: {}", cause.getCause().getMessage());
|
||||
return;
|
||||
}
|
||||
super.exceptionCaught(ctx, cause);
|
||||
}
|
||||
|
||||
private static String extractDomainPrefix(String domain) {
|
||||
if (domain == null || domain.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
int dotIndex = domain.indexOf('.');
|
||||
if (dotIndex == -1) {
|
||||
return domain;
|
||||
}
|
||||
return domain.substring(0, dotIndex);
|
||||
}
|
||||
|
||||
// 方法1: 将HttpRequest序列化为ByteBuf
|
||||
public static ByteBuf convertToByteBuf(HttpRequest request) {
|
||||
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
|
||||
try {
|
||||
// 写入请求行
|
||||
buffer.writeBytes(request.method().name().getBytes());
|
||||
buffer.writeByte(' ');
|
||||
buffer.writeBytes(request.uri().getBytes());
|
||||
buffer.writeByte(' ');
|
||||
buffer.writeBytes(request.protocolVersion().text().getBytes());
|
||||
buffer.writeBytes("\r\n".getBytes());
|
||||
|
||||
// 写入请求头
|
||||
request.headers().forEach(entry -> {
|
||||
buffer.writeBytes(entry.getKey().getBytes());
|
||||
buffer.writeBytes(": ".getBytes());
|
||||
buffer.writeBytes(entry.getValue().getBytes());
|
||||
buffer.writeBytes("\r\n".getBytes());
|
||||
});
|
||||
|
||||
// 写入空行
|
||||
buffer.writeBytes("\r\n".getBytes());
|
||||
|
||||
return buffer;
|
||||
} catch (Exception e) {
|
||||
buffer.release();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.xspaceagi.sandbox.infra.network;
|
||||
|
||||
import com.xspaceagi.sandbox.infra.network.protocol.Constants;
|
||||
import com.xspaceagi.sandbox.infra.network.protocol.ProxyMessage;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* 处理服务端 channel.
|
||||
*/
|
||||
@Slf4j
|
||||
public class UserChannelHandler extends SimpleChannelInboundHandler<ByteBuf> {
|
||||
|
||||
private static final AtomicLong userIdProducer = new AtomicLong(0);
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
||||
|
||||
// 当出现异常就关闭连接
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) {
|
||||
|
||||
// 通知代理客户端
|
||||
Channel userChannel = ctx.channel();
|
||||
Channel proxyChannel = userChannel.attr(Constants.NEXT_CHANNEL).get();
|
||||
if (proxyChannel == null) {
|
||||
|
||||
// 该端口还没有代理客户端
|
||||
ctx.channel().close();
|
||||
} else {
|
||||
byte[] bytes = new byte[buf.readableBytes()];
|
||||
buf.readBytes(bytes);
|
||||
String userId = ProxyChannelManager.getUserChannelUserId(userChannel);
|
||||
ProxyMessage proxyMessage = new ProxyMessage();
|
||||
proxyMessage.setType(ProxyMessage.P_TYPE_TRANSFER);
|
||||
proxyMessage.setUri(userId);
|
||||
proxyMessage.setData(bytes);
|
||||
proxyChannel.writeAndFlush(proxyMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive(ChannelHandlerContext ctx) throws Exception {
|
||||
Channel userChannel = ctx.channel();
|
||||
InetSocketAddress sa = (InetSocketAddress) userChannel.localAddress();
|
||||
Channel cmdChannel = ProxyChannelManager.getCmdChannel(sa.getPort());
|
||||
|
||||
if (cmdChannel == null) {
|
||||
log.warn("no cmd channel for port {}", sa.getPort());
|
||||
// 该端口还没有代理客户端
|
||||
ctx.channel().close();
|
||||
} else {
|
||||
String userId = newUserId();
|
||||
String lanInfo = ProxyChannelManager.getPortLanInfo(sa.getPort());
|
||||
if (lanInfo == null) {
|
||||
log.warn("no lan info for port {}", sa.getPort());
|
||||
ctx.channel().close();
|
||||
return;
|
||||
}
|
||||
// 用户连接到代理服务器时,设置用户连接不可读,等待代理后端服务器连接成功后再改变为可读状态
|
||||
userChannel.config().setOption(ChannelOption.AUTO_READ, false);
|
||||
ProxyChannelManager.addUserChannelToCmdChannel(cmdChannel, userId, userChannel);
|
||||
ProxyMessage proxyMessage = new ProxyMessage();
|
||||
proxyMessage.setType(ProxyMessage.TYPE_CONNECT);
|
||||
proxyMessage.setUri(userId);
|
||||
proxyMessage.setData(lanInfo.getBytes());
|
||||
cmdChannel.writeAndFlush(proxyMessage);
|
||||
}
|
||||
|
||||
super.channelActive(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
||||
|
||||
// 通知代理客户端
|
||||
Channel userChannel = ctx.channel();
|
||||
InetSocketAddress sa = (InetSocketAddress) userChannel.localAddress();
|
||||
Channel cmdChannel = ProxyChannelManager.getCmdChannel(sa.getPort());
|
||||
if (cmdChannel == null) {
|
||||
|
||||
// 该端口还没有代理客户端
|
||||
ctx.channel().close();
|
||||
} else {
|
||||
|
||||
// 用户连接断开,从控制连接中移除
|
||||
String userId = ProxyChannelManager.getUserChannelUserId(userChannel);
|
||||
ProxyChannelManager.removeUserChannelFromCmdChannel(cmdChannel, userId);
|
||||
Channel proxyChannel = userChannel.attr(Constants.NEXT_CHANNEL).get();
|
||||
if (proxyChannel != null && proxyChannel.isActive()) {
|
||||
proxyChannel.attr(Constants.NEXT_CHANNEL).remove();
|
||||
proxyChannel.attr(Constants.CLIENT_KEY).remove();
|
||||
proxyChannel.attr(Constants.USER_ID).remove();
|
||||
|
||||
proxyChannel.config().setOption(ChannelOption.AUTO_READ, true);
|
||||
// 通知客户端,用户连接已经断开
|
||||
ProxyMessage proxyMessage = new ProxyMessage();
|
||||
proxyMessage.setType(ProxyMessage.TYPE_DISCONNECT);
|
||||
proxyMessage.setUri(userId);
|
||||
proxyChannel.writeAndFlush(proxyMessage);
|
||||
}
|
||||
}
|
||||
|
||||
super.channelInactive(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
|
||||
|
||||
// 通知代理客户端
|
||||
Channel userChannel = ctx.channel();
|
||||
InetSocketAddress sa = (InetSocketAddress) userChannel.localAddress();
|
||||
Channel cmdChannel = ProxyChannelManager.getCmdChannel(sa.getPort());
|
||||
if (cmdChannel == null) {
|
||||
|
||||
// 该端口还没有代理客户端
|
||||
ctx.channel().close();
|
||||
} else {
|
||||
Channel proxyChannel = userChannel.attr(Constants.NEXT_CHANNEL).get();
|
||||
if (proxyChannel != null) {
|
||||
proxyChannel.config().setOption(ChannelOption.AUTO_READ, userChannel.isWritable());
|
||||
}
|
||||
}
|
||||
|
||||
super.channelWritabilityChanged(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为用户连接产生ID
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String newUserId() {
|
||||
return String.valueOf(userIdProducer.incrementAndGet());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
package com.xspaceagi.sandbox.infra.network;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelPromise;
|
||||
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Queue;
|
||||
|
||||
/**
|
||||
* WebSocket Frame转换为ByteBuf的Handler
|
||||
* <p>
|
||||
* 该Handler作为透明转换层,位于WebSocket解码器和UserChannelHandler之间
|
||||
* 将WebSocket Frame转换为ByteBuf,使UserChannelHandler无感知地处理WebSocket数据
|
||||
* <p>
|
||||
* 数据流向:
|
||||
* WebSocket Frame -> WebSocketFrameToByteBufHandler -> ByteBuf -> UserChannelHandler
|
||||
*
|
||||
* @author sandbox
|
||||
*/
|
||||
@Slf4j
|
||||
public class WebSocketFrameToByteBufHandler extends ChannelDuplexHandler {
|
||||
|
||||
/**
|
||||
* WebSocket握手是否完成
|
||||
*/
|
||||
private volatile boolean handshakeComplete = false;
|
||||
|
||||
/**
|
||||
* 握手期间的待发送数据队列
|
||||
*/
|
||||
private final java.util.Queue<Object> pendingWrites = new java.util.LinkedList<>();
|
||||
|
||||
/**
|
||||
* 用于累加分片消息的缓冲区
|
||||
*/
|
||||
private ByteBuf fragmentationBuffer;
|
||||
|
||||
/**
|
||||
* 当前分片消息的类型(true为文本,false为二进制)
|
||||
*/
|
||||
private Boolean isTextFragment;
|
||||
|
||||
@Override
|
||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
||||
if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
|
||||
handshakeComplete = true;
|
||||
log.info("🤝 WebSocket handshake completed! Channel: {}, flushing {} pending messages...",
|
||||
ctx.channel().id().asShortText(), pendingWrites.size());
|
||||
|
||||
// 发送所有缓存的数据
|
||||
Object pendingMsg;
|
||||
int count = 0;
|
||||
while ((pendingMsg = pendingWrites.poll()) != null) {
|
||||
ctx.write(pendingMsg);
|
||||
count++;
|
||||
}
|
||||
|
||||
// 批量flush
|
||||
if (count > 0) {
|
||||
ctx.flush();
|
||||
log.info("✅ Flushed {} pending messages after handshake", count);
|
||||
}
|
||||
}
|
||||
super.userEventTriggered(ctx, evt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
if (!(msg instanceof WebSocketFrame)) {
|
||||
// 非WebSocket Frame,直接传递给下一个handler
|
||||
log.trace("Passing non-WebSocket message: {}", msg.getClass().getSimpleName());
|
||||
ctx.fireChannelRead(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
WebSocketFrame frame = (WebSocketFrame) msg;
|
||||
log.trace("Received WebSocket frame: {}", frame.getClass().getSimpleName());
|
||||
|
||||
try {
|
||||
if (frame instanceof CloseWebSocketFrame) {
|
||||
handleCloseFrame(ctx, (CloseWebSocketFrame) frame);
|
||||
} else if (frame instanceof PingWebSocketFrame) {
|
||||
handlePingFrame(ctx, (PingWebSocketFrame) frame);
|
||||
} else if (frame instanceof PongWebSocketFrame) {
|
||||
handlePongFrame(ctx, (PongWebSocketFrame) frame);
|
||||
} else if (frame instanceof TextWebSocketFrame) {
|
||||
handleTextFrame(ctx, (TextWebSocketFrame) frame);
|
||||
} else if (frame instanceof BinaryWebSocketFrame) {
|
||||
handleBinaryFrame(ctx, (BinaryWebSocketFrame) frame);
|
||||
} else if (frame instanceof ContinuationWebSocketFrame) {
|
||||
handleContinuationFrame(ctx, (ContinuationWebSocketFrame) frame);
|
||||
} else {
|
||||
log.warn("Unsupported WebSocket frame type: {}", frame.getClass().getName());
|
||||
frame.release();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing WebSocket frame", e);
|
||||
frame.release();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理关闭帧
|
||||
* Close帧不应该传递给UserChannelHandler,而是直接关闭连接
|
||||
*/
|
||||
private void handleCloseFrame(ChannelHandlerContext ctx, CloseWebSocketFrame frame) {
|
||||
log.info("Received Close WebSocket Frame, closing connection");
|
||||
try {
|
||||
// 回复Close帧
|
||||
ctx.writeAndFlush(frame.retain()).addListener(future -> {
|
||||
ctx.close();
|
||||
log.debug("WebSocket connection closed after receiving Close frame");
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.error("Error handling close frame", e);
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Ping帧
|
||||
*/
|
||||
private void handlePingFrame(ChannelHandlerContext ctx, PingWebSocketFrame frame) {
|
||||
log.debug("Received Ping WebSocket Frame");
|
||||
try {
|
||||
// 自动回复Pong帧
|
||||
ByteBuf content = frame.content();
|
||||
if (content.isReadable()) {
|
||||
ctx.writeAndFlush(new PongWebSocketFrame(content.retainedDuplicate()));
|
||||
} else {
|
||||
ctx.writeAndFlush(new PongWebSocketFrame());
|
||||
}
|
||||
} finally {
|
||||
frame.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Pong帧
|
||||
*/
|
||||
private void handlePongFrame(ChannelHandlerContext ctx, PongWebSocketFrame frame) {
|
||||
log.debug("Received Pong WebSocket Frame");
|
||||
// Pong帧不需要转发给UserChannelHandler
|
||||
frame.release();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文本帧
|
||||
*/
|
||||
private void handleTextFrame(ChannelHandlerContext ctx, TextWebSocketFrame frame) {
|
||||
try {
|
||||
String text = frame.text();
|
||||
log.debug("Received Text WebSocket Frame, length: {}", text.length());
|
||||
|
||||
// 检查是否为分片的最后一帧
|
||||
boolean isFinal = frame.isFinalFragment();
|
||||
|
||||
if (!isFinal) {
|
||||
// 开始接收分片消息
|
||||
if (fragmentationBuffer == null) {
|
||||
fragmentationBuffer = ctx.alloc().buffer();
|
||||
isTextFragment = true;
|
||||
log.debug("Start receiving text fragment");
|
||||
}
|
||||
// 累积分片数据
|
||||
fragmentationBuffer.writeBytes(text.getBytes(io.netty.util.CharsetUtil.UTF_8));
|
||||
} else {
|
||||
// 最后一帧或完整的消息
|
||||
if (fragmentationBuffer != null && Boolean.TRUE.equals(isTextFragment)) {
|
||||
// 分片结束
|
||||
fragmentationBuffer.writeBytes(text.getBytes(io.netty.util.CharsetUtil.UTF_8));
|
||||
// 转发给UserChannelHandler
|
||||
ByteBuf completeBuffer = fragmentationBuffer.retainedSlice();
|
||||
ctx.fireChannelRead(completeBuffer);
|
||||
log.debug("Completed text fragment, total length: {}", fragmentationBuffer.readableBytes());
|
||||
// 释放并重置缓冲区
|
||||
fragmentationBuffer.release();
|
||||
fragmentationBuffer = null;
|
||||
isTextFragment = null;
|
||||
} else {
|
||||
// 单个完整的消息,转换为ByteBuf转发
|
||||
byte[] textBytes = text.getBytes(io.netty.util.CharsetUtil.UTF_8);
|
||||
ByteBuf buffer = Unpooled.wrappedBuffer(textBytes);
|
||||
ctx.fireChannelRead(buffer);
|
||||
log.debug("Forwarded text data as ByteBuf, length: {}", textBytes.length);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
frame.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理二进制帧
|
||||
*/
|
||||
private void handleBinaryFrame(ChannelHandlerContext ctx, BinaryWebSocketFrame frame) {
|
||||
try {
|
||||
ByteBuf binaryData = frame.content();
|
||||
int length = binaryData.readableBytes();
|
||||
log.debug("Received Binary WebSocket Frame, length: {}, final: {}", length, frame.isFinalFragment());
|
||||
|
||||
// 检查是否为分片的最后一帧
|
||||
boolean isFinal = frame.isFinalFragment();
|
||||
|
||||
if (!isFinal) {
|
||||
// 开始接收分片消息
|
||||
if (fragmentationBuffer == null) {
|
||||
fragmentationBuffer = ctx.alloc().buffer();
|
||||
isTextFragment = false;
|
||||
log.info("Start receiving binary fragment");
|
||||
}
|
||||
// 累积分片数据
|
||||
fragmentationBuffer.writeBytes(binaryData);
|
||||
log.trace("Accumulated binary fragment, current size: {}", fragmentationBuffer.readableBytes());
|
||||
} else {
|
||||
// 最后一帧或完整的消息
|
||||
if (fragmentationBuffer != null && Boolean.FALSE.equals(isTextFragment)) {
|
||||
// 分片结束
|
||||
fragmentationBuffer.writeBytes(binaryData);
|
||||
// 转发给UserChannelHandler
|
||||
ByteBuf completeBuffer = fragmentationBuffer.retainedSlice();
|
||||
log.info("Completed binary fragment, total length: {}, forwarding to UserChannelHandler", completeBuffer.readableBytes());
|
||||
ctx.fireChannelRead(completeBuffer);
|
||||
log.trace("Binary fragment successfully forwarded to UserChannelHandler");
|
||||
// 释放并重置缓冲区
|
||||
fragmentationBuffer.release();
|
||||
fragmentationBuffer = null;
|
||||
isTextFragment = null;
|
||||
} else {
|
||||
// 单个完整的消息,直接转发
|
||||
ByteBuf forwardBuffer = binaryData.retain();
|
||||
log.info("Forwarding complete binary message to UserChannelHandler, length: {}", length);
|
||||
ctx.fireChannelRead(forwardBuffer);
|
||||
log.trace("Binary message successfully forwarded to UserChannelHandler");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
frame.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理续传帧(分片消息的中间帧)
|
||||
*/
|
||||
private void handleContinuationFrame(ChannelHandlerContext ctx, ContinuationWebSocketFrame frame) {
|
||||
try {
|
||||
log.debug("Received Continuation WebSocket Frame");
|
||||
|
||||
// 检查是否有正在进行的分片接收
|
||||
if (fragmentationBuffer == null) {
|
||||
log.warn("Received ContinuationFrame without active fragmentation, discarding");
|
||||
return;
|
||||
}
|
||||
|
||||
ByteBuf continuationData = frame.content();
|
||||
boolean isFinal = frame.isFinalFragment();
|
||||
|
||||
// 累加续传数据
|
||||
fragmentationBuffer.writeBytes(continuationData);
|
||||
|
||||
if (isFinal) {
|
||||
// 分片结束,转发完整数据
|
||||
ByteBuf completeBuffer = fragmentationBuffer.retainedSlice();
|
||||
ctx.fireChannelRead(completeBuffer);
|
||||
log.debug("Completed continuation fragment, total length: {}, type: {}",
|
||||
completeBuffer.readableBytes(), isTextFragment ? "TEXT" : "BINARY");
|
||||
// 释放并重置缓冲区
|
||||
fragmentationBuffer.release();
|
||||
fragmentationBuffer = null;
|
||||
isTextFragment = null;
|
||||
}
|
||||
} finally {
|
||||
frame.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写数据时,将ByteBuf转换为WebSocket Frame
|
||||
* 这个方法会在Channel.write()时被调用
|
||||
*/
|
||||
@Override
|
||||
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
|
||||
// 检查WebSocket握手是否完成
|
||||
if (!handshakeComplete) {
|
||||
String msgType = msg.getClass().getSimpleName();
|
||||
|
||||
// 握手期间只缓存需要转换为WebSocket Frame的数据
|
||||
// HTTP相关消息(HttpResponse等)不缓存,让WebSocketServerProtocolHandler处理
|
||||
if (msg instanceof ByteBuf || msg instanceof byte[] || msg instanceof String) {
|
||||
log.debug("⏳ WebSocket handshake in progress, buffering message. Type: {}, Queue size: {}",
|
||||
msgType, pendingWrites.size());
|
||||
|
||||
Object convertedMsg = convertToWebSocketFrame(ctx, msg);
|
||||
if (convertedMsg != null) {
|
||||
pendingWrites.offer(convertedMsg);
|
||||
promise.setSuccess();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP消息(握手响应等)直接透传,不缓存
|
||||
log.trace("Passing through HTTP message during handshake: {}", msgType);
|
||||
ctx.write(msg, promise);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查channel是否可写
|
||||
if (!ctx.channel().isWritable()) {
|
||||
log.warn("⚠️ Channel is NOT writable! Data will be buffered. Channel: {}, write buffer: {}",
|
||||
ctx.channel().id().asShortText(), ctx.channel().bytesBeforeUnwritable());
|
||||
}
|
||||
|
||||
try {
|
||||
Object convertedMsg = convertToWebSocketFrame(ctx, msg);
|
||||
if (convertedMsg != null) {
|
||||
// 关键修复:使用writeAndFlush而不是write
|
||||
ctx.writeAndFlush(convertedMsg, promise);
|
||||
log.trace("✅ WebSocket frame sent successfully, type: {}", convertedMsg.getClass().getSimpleName());
|
||||
} else {
|
||||
// 其他类型直接透传
|
||||
ctx.write(msg, promise);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Error in write, channel: {}, isActive: {}", ctx.channel(), ctx.channel().isActive(), e);
|
||||
promise.setFailure(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将消息转换为WebSocket Frame
|
||||
*/
|
||||
private Object convertToWebSocketFrame(ChannelHandlerContext ctx, Object msg) {
|
||||
if (msg instanceof ByteBuf) {
|
||||
ByteBuf byteBuf = (ByteBuf) msg;
|
||||
log.debug("📤 Convert ByteBuf to BinaryWebSocketFrame, length: {}, channel: {}",
|
||||
byteBuf.readableBytes(), ctx.channel().id().asShortText());
|
||||
return new BinaryWebSocketFrame(byteBuf.retain());
|
||||
|
||||
} else if (msg instanceof byte[]) {
|
||||
byte[] bytes = (byte[]) msg;
|
||||
log.debug("📤 Convert byte[] to BinaryWebSocketFrame, length: {}", bytes.length);
|
||||
return new BinaryWebSocketFrame(Unpooled.wrappedBuffer(bytes));
|
||||
|
||||
} else if (msg instanceof String) {
|
||||
String text = (String) msg;
|
||||
log.debug("📤 Convert String to TextWebSocketFrame, length: {}", text.length());
|
||||
return new TextWebSocketFrame(text);
|
||||
|
||||
} else if (msg instanceof BinaryWebSocketFrame || msg instanceof TextWebSocketFrame) {
|
||||
// 已经是WebSocket Frame,直接返回
|
||||
return msg;
|
||||
|
||||
} else {
|
||||
log.trace("Passing through non-converted message: {}", msg.getClass().getSimpleName());
|
||||
return null; // 表示不需要转换
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush(ChannelHandlerContext ctx) throws Exception {
|
||||
ctx.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
||||
log.debug("WebSocket to ByteBuf converter inactive");
|
||||
cleanupResources();
|
||||
super.channelInactive(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
log.error("WebSocket to ByteBuf converter exception", cause);
|
||||
cleanupResources();
|
||||
ctx.fireExceptionCaught(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
private void cleanupResources() {
|
||||
if (fragmentationBuffer != null) {
|
||||
fragmentationBuffer.release();
|
||||
fragmentationBuffer = null;
|
||||
}
|
||||
isTextFragment = null;
|
||||
|
||||
// 清空待发送队列
|
||||
if (!pendingWrites.isEmpty()) {
|
||||
log.warn("⚠️ Cleaning up {} pending messages due to channel inactive", pendingWrites.size());
|
||||
Object msg;
|
||||
while ((msg = pendingWrites.poll()) != null) {
|
||||
if (msg instanceof io.netty.util.ReferenceCounted) {
|
||||
((io.netty.util.ReferenceCounted) msg).release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.xspaceagi.sandbox.infra.network.protocol;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.util.AttributeKey;
|
||||
|
||||
import java.util.Queue;
|
||||
|
||||
public interface Constants {
|
||||
|
||||
AttributeKey<Channel> NEXT_CHANNEL = AttributeKey.newInstance("nxt_channel");
|
||||
|
||||
AttributeKey<String> USER_ID = AttributeKey.newInstance("user_id");
|
||||
|
||||
AttributeKey<String> CLIENT_KEY = AttributeKey.newInstance("client_key");
|
||||
|
||||
AttributeKey<Queue<Object>> MESSAGE_QUEUE = AttributeKey.newInstance("message_queue");
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.xspaceagi.sandbox.infra.network.protocol;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* check idle chanel.
|
||||
*
|
||||
* @author fengfei
|
||||
*
|
||||
*/
|
||||
public class IdleCheckHandler extends IdleStateHandler {
|
||||
|
||||
public static final int USER_CHANNEL_READ_IDLE_TIME = 1200;
|
||||
|
||||
public static final int READ_IDLE_TIME = 60;
|
||||
|
||||
public static final int WRITE_IDLE_TIME = 40;
|
||||
|
||||
private static Logger logger = LoggerFactory.getLogger(IdleCheckHandler.class);
|
||||
|
||||
public IdleCheckHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds) {
|
||||
super(readerIdleTimeSeconds, writerIdleTimeSeconds, allIdleTimeSeconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
|
||||
|
||||
if (IdleStateEvent.FIRST_WRITER_IDLE_STATE_EVENT == evt) {
|
||||
logger.debug("channel write timeout {}", ctx.channel());
|
||||
ProxyMessage proxyMessage = new ProxyMessage();
|
||||
proxyMessage.setType(ProxyMessage.TYPE_HEARTBEAT);
|
||||
ctx.channel().writeAndFlush(proxyMessage);
|
||||
} else if (IdleStateEvent.FIRST_READER_IDLE_STATE_EVENT == evt) {
|
||||
logger.warn("channel read timeout {}", ctx.channel());
|
||||
ctx.channel().close();
|
||||
}
|
||||
super.channelIdle(ctx, evt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.xspaceagi.sandbox.infra.network.protocol;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 代理客户端与代理服务器消息交换协议
|
||||
*
|
||||
* @author fengfei
|
||||
*
|
||||
*/
|
||||
public class ProxyMessage {
|
||||
|
||||
/** 心跳消息 */
|
||||
public static final byte TYPE_HEARTBEAT = 0x07;
|
||||
|
||||
/** 认证消息,检测clientKey是否正确 */
|
||||
public static final byte C_TYPE_AUTH = 0x01;
|
||||
|
||||
// /** 保活确认消息 */
|
||||
// public static final byte TYPE_ACK = 0x02;
|
||||
|
||||
/** 代理后端服务器建立连接消息 */
|
||||
public static final byte TYPE_CONNECT = 0x03;
|
||||
|
||||
/** 代理后端服务器断开连接消息 */
|
||||
public static final byte TYPE_DISCONNECT = 0x04;
|
||||
|
||||
/** 代理数据传输 */
|
||||
public static final byte P_TYPE_TRANSFER = 0x05;
|
||||
|
||||
/** 用户与代理服务器以及代理客户端与真实服务器连接是否可写状态同步 */
|
||||
public static final byte C_TYPE_WRITE_CONTROL = 0x06;
|
||||
|
||||
/** 消息类型 */
|
||||
private byte type;
|
||||
|
||||
/** 消息流水号 */
|
||||
private long serialNumber;
|
||||
|
||||
/** 消息命令请求信息 */
|
||||
private String uri;
|
||||
|
||||
/** 消息传输数据 */
|
||||
private byte[] data;
|
||||
|
||||
public void setUri(String uri) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
public String getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(byte[] data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public byte getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(byte type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public long getSerialNumber() {
|
||||
return serialNumber;
|
||||
}
|
||||
|
||||
public void setSerialNumber(long serialNumber) {
|
||||
this.serialNumber = serialNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ProxyMessage [type=" + type + ", serialNumber=" + serialNumber + ", uri=" + uri + ", data=" + Arrays.toString(data) + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.xspaceagi.sandbox.infra.network.protocol;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
|
||||
|
||||
public class ProxyMessageDecoder extends LengthFieldBasedFrameDecoder {
|
||||
|
||||
private static final byte HEADER_SIZE = 4;
|
||||
|
||||
private static final int TYPE_SIZE = 1;
|
||||
|
||||
private static final int SERIAL_NUMBER_SIZE = 8;
|
||||
|
||||
private static final int URI_LENGTH_SIZE = 1;
|
||||
|
||||
/**
|
||||
* @param maxFrameLength
|
||||
* @param lengthFieldOffset
|
||||
* @param lengthFieldLength
|
||||
* @param lengthAdjustment
|
||||
* @param initialBytesToStrip
|
||||
*/
|
||||
public ProxyMessageDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment,
|
||||
int initialBytesToStrip) {
|
||||
super(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param maxFrameLength
|
||||
* @param lengthFieldOffset
|
||||
* @param lengthFieldLength
|
||||
* @param lengthAdjustment
|
||||
* @param initialBytesToStrip
|
||||
* @param failFast
|
||||
*/
|
||||
public ProxyMessageDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment,
|
||||
int initialBytesToStrip, boolean failFast) {
|
||||
super(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip, failFast);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ProxyMessage decode(ChannelHandlerContext ctx, ByteBuf in2) throws Exception {
|
||||
ByteBuf in = (ByteBuf) super.decode(ctx, in2);
|
||||
if (in == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (in.readableBytes() < HEADER_SIZE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int frameLength = in.readInt();
|
||||
if (in.readableBytes() < frameLength) {
|
||||
return null;
|
||||
}
|
||||
ProxyMessage proxyMessage = new ProxyMessage();
|
||||
byte type = in.readByte();
|
||||
long sn = in.readLong();
|
||||
|
||||
proxyMessage.setSerialNumber(sn);
|
||||
|
||||
proxyMessage.setType(type);
|
||||
|
||||
byte uriLength = in.readByte();
|
||||
byte[] uriBytes = new byte[uriLength];
|
||||
in.readBytes(uriBytes);
|
||||
proxyMessage.setUri(new String(uriBytes));
|
||||
|
||||
byte[] data = new byte[frameLength - TYPE_SIZE - SERIAL_NUMBER_SIZE - URI_LENGTH_SIZE - uriLength];
|
||||
in.readBytes(data);
|
||||
proxyMessage.setData(data);
|
||||
|
||||
in.release();
|
||||
|
||||
return proxyMessage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.xspaceagi.sandbox.infra.network.protocol;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.MessageToByteEncoder;
|
||||
|
||||
public class ProxyMessageEncoder extends MessageToByteEncoder<ProxyMessage> {
|
||||
|
||||
private static final int TYPE_SIZE = 1;
|
||||
|
||||
private static final int SERIAL_NUMBER_SIZE = 8;
|
||||
|
||||
private static final int URI_LENGTH_SIZE = 1;
|
||||
|
||||
@Override
|
||||
protected void encode(ChannelHandlerContext ctx, ProxyMessage msg, ByteBuf out) throws Exception {
|
||||
int bodyLength = TYPE_SIZE + SERIAL_NUMBER_SIZE + URI_LENGTH_SIZE;
|
||||
byte[] uriBytes = null;
|
||||
if (msg.getUri() != null) {
|
||||
uriBytes = msg.getUri().getBytes();
|
||||
bodyLength += uriBytes.length;
|
||||
}
|
||||
|
||||
if (msg.getData() != null) {
|
||||
bodyLength += msg.getData().length;
|
||||
}
|
||||
|
||||
// write the total packet length but without length field's length.
|
||||
out.writeInt(bodyLength);
|
||||
|
||||
out.writeByte(msg.getType());
|
||||
out.writeLong(msg.getSerialNumber());
|
||||
|
||||
if (uriBytes != null) {
|
||||
out.writeByte((byte) uriBytes.length);
|
||||
out.writeBytes(uriBytes);
|
||||
} else {
|
||||
out.writeByte((byte) 0x00);
|
||||
}
|
||||
|
||||
if (msg.getData() != null) {
|
||||
out.writeBytes(msg.getData());
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user