diff --git a/neutrino-proxy-admin/src/views/proxy/domainMapping.vue b/neutrino-proxy-admin/src/views/proxy/domainMapping.vue index 5e71a596..bd8a6c7c 100644 --- a/neutrino-proxy-admin/src/views/proxy/domainMapping.vue +++ b/neutrino-proxy-admin/src/views/proxy/domainMapping.vue @@ -7,10 +7,10 @@ - + + @@ -277,6 +277,7 @@ export default { listQuery: { current: 1, size: 10, + domain: null, importance: undefined, title: undefined, type: undefined, diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/DomainController.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/DomainController.java new file mode 100644 index 00000000..4e94c5ea --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/DomainController.java @@ -0,0 +1,128 @@ +package org.dromara.neutrinoproxy.server.controller; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.dromara.neutrinoproxy.server.base.page.PageInfo; +import org.dromara.neutrinoproxy.server.base.page.PageQuery; +import org.dromara.neutrinoproxy.server.base.proxy.ProxyConfig; +import org.dromara.neutrinoproxy.server.constant.ExceptionConstant; +import org.dromara.neutrinoproxy.server.constant.NetworkProtocolEnum; +import org.dromara.neutrinoproxy.server.controller.req.proxy.*; +import org.dromara.neutrinoproxy.server.controller.res.proxy.*; +import org.dromara.neutrinoproxy.server.service.DomainMappingService; +import org.dromara.neutrinoproxy.server.service.PortMappingService; +import org.dromara.neutrinoproxy.server.util.ParamCheckUtil; +import org.noear.solon.annotation.*; + +/** + * @author: aoshiguchen + * @date: 2023/4/2 + */ +@Slf4j +@Mapping("/domain") +@Controller +public class DomainController { + @Inject + private ProxyConfig proxyConfig; + + @Inject + private DomainMappingService domainMappingService; + + @Get + @Mapping("/bind-info") + public String bindInfo () { + return proxyConfig.getServer().getTcp().getDomainName(); + } + + + @Get + @Mapping("/page") + public PageInfo page(PageQuery pageQuery, PortMappingListReq req) { + ParamCheckUtil.checkNotNull(pageQuery, "pageQuery"); + return domainMappingService.page(pageQuery, req); + } + + @Post + @Mapping("/modify") + public PortMappingCreateRes create(PortMappingCreateReq req) { + ParamCheckUtil.checkNotNull(req, "req"); + ParamCheckUtil.checkNotNull(req.getLicenseId(), "licenseId"); + ParamCheckUtil.checkNotNull(req.getServerPort(), "serverPort"); + ParamCheckUtil.checkNotNull(req.getClientPort(), "clientPort"); + ParamCheckUtil.checkNotEmpty(req.getProtocal(), "protocal"); + ParamCheckUtil.checkMaxLength(req.getDescription(), 50, "描述", "50"); + ParamCheckUtil.checkBytesDesc(req.getUpLimitRate(), "upLimitRate"); + ParamCheckUtil.checkBytesDesc(req.getDownLimitRate(), "downLimitRate"); + if (StringUtils.isBlank(req.getClientIp())) { + // 没传客户端ip,默认为127.0.0.1 + req.setClientIp("127.0.0.1"); + } + NetworkProtocolEnum networkProtocolEnum = NetworkProtocolEnum.of(req.getProtocal()); + ParamCheckUtil.checkNotNull(networkProtocolEnum, ExceptionConstant.AN_UNSUPPORTED_PROTOCOL, req.getProtocal()); + if (networkProtocolEnum != NetworkProtocolEnum.HTTP) { + // 目前仅HTTP支持绑定域名 + req.setSubdomain(null); + } + req.setProtocal(networkProtocolEnum.getDesc()); + if (null == req.getProxyResponses()) { + req.setProxyResponses(0); + } + if (null == req.getProxyTimeoutMs()) { + req.setProxyTimeoutMs(0L); + } + if (null == req.getSecurityGroupId()) { + req.setSecurityGroupId(0); + } + + return domainMappingService.create(req); + } + + @Get + @Mapping("/one/{id}") + public DomainMappingDto one(Integer id) { + return domainMappingService.one(id); + } + + @Post + @Mapping("/update/enable-status") + public PortMappingUpdateEnableStatusRes updateEnableStatus(PortMappingUpdateEnableStatusReq req) { + ParamCheckUtil.checkNotNull(req, "req"); + ParamCheckUtil.checkNotNull(req.getId(), "id"); + ParamCheckUtil.checkNotNull(req.getEnable(), "enable"); + + return domainMappingService.updateEnableStatus(req); + } + + @Post + @Mapping("/delete") + public void delete(PortMappingDeleteReq req) { + ParamCheckUtil.checkNotNull(req, "req"); + ParamCheckUtil.checkNotNull(req.getId(), "id"); + + domainMappingService.delete(req.getId()); + } + + /** + * 绑定安全组 + * @param req portMappingId和securityGroupId + */ + @Post + @Mapping("/bind/security-group") + public void bindSecurityGroup(PortMappingBindSecurityGroupReq req) { + domainMappingService.portBindSecurityGroup(req.getId(), req.getSecurityGroupId()); + } + + /** + * 安全组解绑 + * @param id 端口映射Id + */ + @Post + @Mapping("/unbind/security-group") + public void unbindSecurityGroup(Integer id) { + domainMappingService.portUnbindSecurityGroup(id); + } + + + + +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/DomainNameController.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/DomainNameController.java deleted file mode 100644 index 319a4e5b..00000000 --- a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/DomainNameController.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.dromara.neutrinoproxy.server.controller; - -import lombok.extern.slf4j.Slf4j; -import org.dromara.neutrinoproxy.server.base.proxy.ProxyConfig; -import org.noear.solon.annotation.Controller; -import org.noear.solon.annotation.Get; -import org.noear.solon.annotation.Inject; -import org.noear.solon.annotation.Mapping; - -/** - * @author: aoshiguchen - * @date: 2023/4/2 - */ -@Slf4j -@Mapping("/domain") -@Controller -public class DomainNameController { - @Inject - private ProxyConfig proxyConfig; - - @Get - @Mapping("/bind-info") - public String bindInfo () { - return proxyConfig.getServer().getTcp().getDomainName(); - } - -} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/res/proxy/DomainMappingDto.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/res/proxy/DomainMappingDto.java new file mode 100644 index 00000000..ad05d7fe --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/res/proxy/DomainMappingDto.java @@ -0,0 +1,31 @@ +package org.dromara.neutrinoproxy.server.controller.res.proxy; + + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dromara.neutrinoproxy.server.dal.entity.DomainMappingDO; + +/** + * + * @author xiaojie + * @date + */ +@Accessors(chain = true) +@Data +public class DomainMappingDto extends DomainMappingDO { + + /** + * licenseId + */ + private String licenseName; + + /** + * 用户ID + */ + private Integer userId; + + /** + * 用户名称 + */ + private String userName; +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/DomainMappingMapper.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/DomainMappingMapper.java new file mode 100644 index 00000000..bf2c44a3 --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/DomainMappingMapper.java @@ -0,0 +1,112 @@ +package org.dromara.neutrinoproxy.server.dal; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.dromara.neutrinoproxy.server.constant.EnableStatusEnum; +import org.dromara.neutrinoproxy.server.controller.req.proxy.PortMappingListReq; +import org.dromara.neutrinoproxy.server.dal.entity.DomainMappingDO; +import org.dromara.neutrinoproxy.server.dal.entity.DomainMappingDO; + +import java.util.Date; +import java.util.List; +import java.util.Set; + +/** + * + * @author: aoshiguchen + * @date: 2022/8/8 + */ +@Mapper +public interface DomainMappingMapper extends BaseMapper { + + default DomainMappingDO findById(Integer id) { + return this.selectById(id); + } + + default void updateEnableStatus(Integer id, Integer enable, Date updateTime) { + this.update(null, new LambdaUpdateWrapper() + .eq(DomainMappingDO::getId, id) + .set(DomainMappingDO::getEnable, enable) + .set(DomainMappingDO::getUpdateTime, updateTime) + ); + } + + default DomainMappingDO findByDomain(String domain, Set excludeIds) { + return this.selectOne(new LambdaQueryWrapper() + .eq(DomainMappingDO::getDomain, domain) + .notIn(!CollectionUtil.isEmpty(excludeIds), DomainMappingDO::getId, excludeIds) + .last("limit 1") + ); + } + + /** + * 校验域名是否重复 + * @param subdomain + * @return + */ + default Boolean checkRepeatByDomain(String subdomain, Set excludeIds) { + return StringUtils.isEmpty(subdomain)? Boolean.FALSE : this.selectCount(new LambdaQueryWrapper() + .eq(DomainMappingDO::getDomain, subdomain) + .notIn(!CollectionUtil.isEmpty(excludeIds), DomainMappingDO::getId, excludeIds) + ).intValue() > 0; + } + + default List findEnableListByLicenseId(Integer licenseId) { + return this.selectList(new LambdaQueryWrapper() + .eq(DomainMappingDO::getLicenseId, licenseId) + .eq(DomainMappingDO::getEnable, EnableStatusEnum.ENABLE.getStatus()) + ); + } + + default List findListByServerPort(Integer serverPort) { + return this.selectList(new LambdaQueryWrapper() + .eq(DomainMappingDO::getServerPort, serverPort) + ); + } + + default List findListByLicenseId(Integer licenseId) { + return this.selectList(new LambdaQueryWrapper() + .eq(DomainMappingDO::getLicenseId, licenseId) + ); + } + + default DomainMappingDO findByLicenseIdAndServerPort(Integer licenseId, Integer serverPort) { + return this.selectOne(new LambdaQueryWrapper() + .eq(DomainMappingDO::getLicenseId, licenseId) + .eq(DomainMappingDO::getServerPort, serverPort) + .last("limit 1") + ); + } + + default void updateOnlineStatus(Integer licenseId,Integer serverPort, Integer isOnline, Date updateTime) { + this.update(null, new LambdaUpdateWrapper() + .eq(DomainMappingDO::getLicenseId, licenseId) + .eq(DomainMappingDO::getServerPort, serverPort) + .set(DomainMappingDO::getIsOnline, isOnline) + .set(DomainMappingDO::getUpdateTime, updateTime) + ); + } + + default void updateOnlineStatus(Integer licenseId, Integer isOnline, Date updateTime) { + this.update(null, new LambdaUpdateWrapper() + .eq(DomainMappingDO::getLicenseId, licenseId) + .set(DomainMappingDO::getIsOnline, isOnline) + .set(DomainMappingDO::getUpdateTime, updateTime) + ); + } + + default void updateOnlineStatus(Integer isOnline, Date updateTime) { + this.update(null, new LambdaUpdateWrapper() + .set(DomainMappingDO::getIsOnline, isOnline) + .set(DomainMappingDO::getUpdateTime, updateTime) + ); + } + + List selectDomainMappingByCondition(IPage page, @Param("req") PortMappingListReq req); +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/entity/DomainMappingDO.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/entity/DomainMappingDO.java new file mode 100644 index 00000000..c87ceb1a --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/entity/DomainMappingDO.java @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2022 aoshiguchen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.dromara.neutrinoproxy.server.dal.entity; + +import cn.hutool.core.bean.BeanUtil; +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.Data; +import lombok.ToString; +import lombok.experimental.Accessors; +import org.dromara.neutrinoproxy.server.constant.EnableStatusEnum; +import org.dromara.neutrinoproxy.server.constant.OnlineStatusEnum; +import org.dromara.neutrinoproxy.server.controller.res.proxy.DomainMappingDto; +import org.dromara.neutrinoproxy.server.controller.res.proxy.PortMappingListRes; + +import java.util.Date; + +/** + * 端口映射 + * @author: aoshiguchen + * @date: 2022/8/8 + */ +@ToString +@Accessors(chain = true) +@Data +@TableName("domain_mapping") +public class DomainMappingDO { + @TableId(type = IdType.AUTO) + private Integer id; + /** + * licenseId + */ + private Integer licenseId; + /** + * 协议 + */ + private String protocal; + /** + * 域名 + */ + private String domain; + /** + * 描述 + */ + private String description; + /** + * 目标地址(licenseId客户端,ip:port) + */ + private String targetPath; + /** + * 上传限速 + */ + private String upLimitRate; + /** + * 下载限速 + */ + private String downLimitRate; + + /** + * 是否在线 + * {@link OnlineStatusEnum} + */ + @TableField(exist=false) + private Integer isOnline; + + /** + * 启用状态 + * {@link EnableStatusEnum} + */ + private Integer enable; + + /** + * 安全组Id + */ + private Integer securityGroupId = 0; // 设置为null不生效,不知道为啥 + + /** + * 创建时间 + */ + private Date createTime; + /** + * 更新时间 + */ + private Date updateTime; + + public DomainMappingDto toRes() { + return (DomainMappingDto) this; + } +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/service/DomainMappingService.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/service/DomainMappingService.java new file mode 100644 index 00000000..9d92a1cb --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/service/DomainMappingService.java @@ -0,0 +1,441 @@ +package org.dromara.neutrinoproxy.server.service; + +import cn.hutool.cache.Cache; +import cn.hutool.cache.CacheUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.solon.plugins.pagination.Page; +import com.google.common.collect.Sets; +import org.apache.ibatis.solon.annotation.Db; +import org.dromara.neutrinoproxy.server.base.db.DBInitialize; +import org.dromara.neutrinoproxy.server.base.page.PageInfo; +import org.dromara.neutrinoproxy.server.base.page.PageQuery; +import org.dromara.neutrinoproxy.server.base.proxy.ProxyConfig; +import org.dromara.neutrinoproxy.server.base.rest.SystemContextHolder; +import org.dromara.neutrinoproxy.server.constant.EnableStatusEnum; +import org.dromara.neutrinoproxy.server.constant.ExceptionConstant; +import org.dromara.neutrinoproxy.server.constant.NetworkProtocolEnum; +import org.dromara.neutrinoproxy.server.constant.OnlineStatusEnum; +import org.dromara.neutrinoproxy.server.controller.req.proxy.PortMappingCreateReq; +import org.dromara.neutrinoproxy.server.controller.req.proxy.PortMappingListReq; +import org.dromara.neutrinoproxy.server.controller.req.proxy.PortMappingUpdateEnableStatusReq; +import org.dromara.neutrinoproxy.server.controller.req.proxy.PortMappingUpdateReq; +import org.dromara.neutrinoproxy.server.controller.res.proxy.*; +import org.dromara.neutrinoproxy.server.dal.DomainMappingMapper; +import org.dromara.neutrinoproxy.server.dal.LicenseMapper; +import org.dromara.neutrinoproxy.server.dal.PortPoolMapper; +import org.dromara.neutrinoproxy.server.dal.UserMapper; +import org.dromara.neutrinoproxy.server.dal.entity.*; +import org.dromara.neutrinoproxy.server.service.bo.FlowLimitBO; +import org.dromara.neutrinoproxy.server.util.ParamCheckUtil; +import org.dromara.neutrinoproxy.server.util.ProxyUtil; +import org.dromara.neutrinoproxy.server.util.StringUtil; +import org.noear.solon.annotation.Component; +import org.noear.solon.annotation.Init; +import org.noear.solon.annotation.Inject; +import org.noear.solon.core.bean.LifecycleBean; +import org.noear.solon.core.runtime.NativeDetector; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * + * 域名解析 service + * author xiaojie + */ +@Component +public class DomainMappingService implements LifecycleBean { + @Db + private DomainMappingMapper domainMappingMapper; + @Db + private LicenseMapper licenseMapper; + @Db + private UserMapper userMapper; + @Db + private PortPoolMapper portPoolMapper; + @Inject + private VisitorChannelService visitorChannelService; + + @Inject + private PortPoolService portPoolService; + @Inject + private ProxyConfig proxyConfig; + @Inject + private DBInitialize dbInitialize; + @Inject + private LicenseService licenseService; + + /** 端口到安全组Id的映射 */ + private final Map mappingDomainToSecurityGroupMap = new ConcurrentHashMap<>(); + /** + * 域名 到 域名解析id的映射 + */ + private final Cache domainToDomainMappingIdCache = CacheUtil.newLRUCache(500, 1000 * 60 * 10); + // 域名解析id到licenseId + private final Cache idToLicenseIdCache = CacheUtil.newLRUCache(500, 1000 * 60 * 10); + // 流量限制缓存 + private final Cache flowLimitCache = CacheUtil.newLRUCache(500, 1000 * 60 * 5); + + public PageInfo page(PageQuery pageQuery, PortMappingListReq req) { + if (StringUtils.isNotEmpty(req.getDescription())) { + //描述字段为模糊查询,在应用层处理,否则sqlite不支持 + req.setDescription("%" + req.getDescription() + "%"); + } + + // 协议名称转换 + if (StringUtils.isNotBlank(req.getProtocal())) { + NetworkProtocolEnum networkProtocolEnum = NetworkProtocolEnum.of(req.getProtocal()); + req.setProtocal(networkProtocolEnum.getDesc()); + } + + Page page = new Page<>(pageQuery.getCurrent(), pageQuery.getSize()); + List list = domainMappingMapper.selectDomainMappingByCondition(page, req); + List respList = list.stream().map(DomainMappingDO::toRes).collect(Collectors.toList()); + if (CollectionUtils.isEmpty(list)) { + return PageInfo.of(respList, page.getTotal(), pageQuery.getCurrent(), pageQuery.getSize()); + } + + Set licenseIds = respList.stream().map(DomainMappingDto::getLicenseId).collect(Collectors.toSet()); + List licenseList = licenseMapper.findByIds(licenseIds); + if (CollectionUtil.isEmpty(licenseList)) { + return PageInfo.of(respList, page.getTotal(), pageQuery.getCurrent(), pageQuery.getSize()); + } + Set userIds = licenseList.stream().map(LicenseDO::getUserId).collect(Collectors.toSet()); + List userList = userMapper.findByIds(userIds); + Map licenseMap = licenseList.stream().collect(Collectors.toMap(LicenseDO::getId, Function.identity())); + Map userMap = userList.stream().collect(Collectors.toMap(UserDO::getId, Function.identity())); + + respList.forEach(item -> { + LicenseDO license = licenseMap.get(item.getLicenseId()); + if (null == license) { + return; + } + item.setLicenseName(license.getName()); + item.setUserId(license.getUserId()); + UserDO user = userMap.get(license.getUserId()); + if (null == user) { + return; + } + item.setUserName(user.getName()); +// if (StrUtil.isNotBlank(proxyConfig.getServer().getTcp().getDomainName()) && StrUtil.isNotBlank(item.getDomain())) { +// item.setDomain(item.getDomain() + "." + proxyConfig.getServer().getTcp().getDomainName()); +// } + if (NetworkProtocolEnum.HTTP.getDesc().equals(item.getProtocal())) { + item.setProtocal("HTTP(S)"); + } + }); + //sorted [userId asc] [licenseId asc] [createTime asc] + respList = respList.stream() + .filter(e -> null != e.getUserId()) + .sorted(Comparator.comparing(DomainMappingDto::getUserId).thenComparing(DomainMappingDto::getLicenseId).thenComparing(DomainMappingDto::getCreateTime)).collect(Collectors.toList()); + return PageInfo.of(respList, page.getTotal(), pageQuery.getCurrent(), pageQuery.getSize()); + } + + public PortMappingCreateRes modify(DomainMappingDto req) { + LicenseDO licenseDO = licenseMapper.findById(req.getLicenseId()); + ParamCheckUtil.checkNotNull(licenseDO, ExceptionConstant.LICENSE_NOT_EXIST); + if (!SystemContextHolder.isAdmin()) { + // 临时处理,如果当前用户不是管理员,则操作userId不能为1 + ParamCheckUtil.checkExpression(!licenseDO.getUserId().equals(1), ExceptionConstant.NO_PERMISSION_VISIT); + } + PortPoolDO portPoolDO = portPoolMapper.findByPort(req.getDomain()); + ParamCheckUtil.checkNotNull(portPoolDO, ExceptionConstant.PORT_NOT_EXIST); + ParamCheckUtil.checkExpression(null == domainMappingMapper.findByDomain(req.getDomain(), null), ExceptionConstant.PORT_CANNOT_REPEAT_MAPPING, req.getDomain()); + ParamCheckUtil.checkExpression(!domainMappingMapper.checkRepeatByDomain(req.getDomain(), null), ExceptionConstant.PORT_MAPPING_SUBDONAME_CONNOT_REPEAT); + + Date now = new Date(); + PortMappingDO portMappingDO = ; + portMappingDO.setLicenseId(req.getLicenseId()); + portMappingDO.setProtocal(req.getProtocal()); + portMappingDO.setSubdomain(req.getSubdomain()); + portMappingDO.setServerPort(req.getServerPort()); + portMappingDO.setClientIp(req.getClientIp()); + portMappingDO.setClientPort(req.getClientPort()); + portMappingDO.setUpLimitRate(req.getUpLimitRate()); + portMappingDO.setDownLimitRate(req.getDownLimitRate()); + portMappingDO.setProxyResponses(req.getProxyResponses()); + portMappingDO.setProxyTimeoutMs(req.getProxyTimeoutMs()); + portMappingDO.setDescription(req.getDescription()); + portMappingDO.setIsOnline(OnlineStatusEnum.OFFLINE.getStatus()); + portMappingDO.setEnable(EnableStatusEnum.ENABLE.getStatus()); + portMappingDO.setCreateTime(now); + portMappingDO.setUpdateTime(now); + domainMappingMapper.insert(portMappingDO); + // 更新VisitorChannel + visitorChannelService.addVisitorChannelByPortMapping(portMappingDO); + // 更新域名映射 + if (NetworkProtocolEnum.isHttp(portMappingDO.getProtocal()) && StrUtil.isNotBlank(proxyConfig.getServer().getTcp().getDomainName()) && StrUtil.isNotBlank(portMappingDO.getSubdomain())) { + ProxyUtil.setSubdomainToServerPort(portMappingDO.getSubdomain(), portMappingDO.getServerPort()); + } + + updateMappingDomainToSecurityGroupMap(portMappingDO.getServerPort(), req.getSecurityGroupId()); + + // 更新端口到映射的缓存 + domainToDomainMappingIdCache.put(req.getServerPort(), portMappingDO.getId()); + // 更新端口映射到licenseId的缓存 + idToLicenseIdCache.put(portMappingDO.getId(), portMappingDO.getLicenseId()); + // 刷新流量限制缓存 + refreshFlowLimitCache(portMappingDO.getId(), portMappingDO.getUpLimitRate(), portMappingDO.getDownLimitRate()); + + return new PortMappingCreateRes(); + } + + + + public PortMappingUpdateEnableStatusRes updateEnableStatus(PortMappingUpdateEnableStatusReq req) { + PortMappingDO portMappingDO = domainMappingMapper.findById(req.getId()); + ParamCheckUtil.checkNotNull(portMappingDO, ExceptionConstant.PORT_MAPPING_NOT_EXIST); + + LicenseDO licenseDO = licenseMapper.findById(portMappingDO.getLicenseId()); + ParamCheckUtil.checkNotNull(licenseDO, ExceptionConstant.LICENSE_NOT_EXIST); + if (!SystemContextHolder.isAdmin()) { + ParamCheckUtil.checkExpression(!licenseDO.getUserId().equals(1), ExceptionConstant.NO_PERMISSION_VISIT); + } + + domainMappingMapper.updateEnableStatus(req.getId(), req.getEnable(), new Date()); + + // 更新VisitorChannel + portMappingDO.setEnable(req.getEnable()); + if (EnableStatusEnum.ENABLE == EnableStatusEnum.of(req.getEnable())) { + visitorChannelService.addVisitorChannelByPortMapping(portMappingDO); + } else { + visitorChannelService.removeVisitorChannelByPortMapping(portMappingDO); + } + + return new PortMappingUpdateEnableStatusRes(); + } + + public void delete(Integer id) { + PortMappingDO portMappingDO = domainMappingMapper.findById(id); + ParamCheckUtil.checkNotNull(portMappingDO, ExceptionConstant.PORT_MAPPING_NOT_EXIST); + + LicenseDO licenseDO = licenseMapper.findById(portMappingDO.getLicenseId()); + if (null != licenseDO && !SystemContextHolder.isAdmin()) { + // 临时处理,如果当前用户不是管理员,则操作userId不能为1 + ParamCheckUtil.checkExpression(!licenseDO.getUserId().equals(1), ExceptionConstant.NO_PERMISSION_VISIT); + } + + domainMappingMapper.deleteById(id); + + // 更新VisitorChannel + visitorChannelService.removeVisitorChannelByPortMapping(portMappingDO); + // 更新域名映射 + if (NetworkProtocolEnum.isHttp(portMappingDO.getProtocal()) && StrUtil.isNotBlank(portMappingDO.getSubdomain())) { + ProxyUtil.removeSubdomainToServerPort(portMappingDO.getSubdomain()); + } + + updateMappingDomainToSecurityGroupMap(portMappingDO.getServerPort(), null); + + // 删除id到licenseId的映射 + idToLicenseIdCache.remove(id); + // 删除流量限制缓存 + flowLimitCache.remove(id); + } + + public void portBindSecurityGroup(Integer portMappingId, Integer groupId) { + PortMappingDO mappingDO = domainMappingMapper.findById(portMappingId); + if (mappingDO == null) { + throw new RuntimeException("指定的端口映射不存在"); + } + mappingDO.setSecurityGroupId(groupId); + mappingDO.setUpdateTime(new Date()); + domainMappingMapper.updateById(mappingDO); + updateMappingDomainToSecurityGroupMap(mappingDO.getServerPort(), groupId); + } + + public void portUnbindSecurityGroup(Integer portMappingId) { + PortMappingDO mappingDO = domainMappingMapper.findById(portMappingId); + if (mappingDO == null) { + throw new RuntimeException("指定的端口映射不存在"); + } + mappingDO.setSecurityGroupId(0); + mappingDO.setUpdateTime(new Date()); + domainMappingMapper.updateById(mappingDO); + updateMappingDomainToSecurityGroupMap(mappingDO.getServerPort(), null); + } + + /** + * 根据license查询可用的端口映射列表 + * + * @param licenseId + * @return + */ + public List findEnableListByLicenseId(Integer licenseId) { + return domainMappingMapper.findEnableListByLicenseId(licenseId); + } + + public Integer getSecurityGroupIdByMappingPort(Integer port) { + return mappingDomainToSecurityGroupMap.get(port); + } + + + /** + * 服务端项目停止、启动时,更新在线状态为离线 + */ + @Init + public void init() { + // aot 阶段,不初始化 + if (NativeDetector.isAotRuntime()) { + return; + } + // 服务刚启动,所以默认所有license都是离线状态。解决服务突然关闭,在线状态来不及更新的问题 + domainMappingMapper.updateOnlineStatus(OnlineStatusEnum.OFFLINE.getStatus(), new Date()); + + List allMappingDOList = domainMappingMapper.selectList(Wrappers.lambdaQuery(DomainMappingDO.class)); + allMappingDOList.forEach(item -> { + Integer securityGroupId = item.getSecurityGroupId(); + if (securityGroupId != null && securityGroupId > 0) { + updateMappingDomainToSecurityGroupMap(item.getDomain(), item.getSecurityGroupId()); + } + // 更新端口到映射的缓存 + domainToDomainMappingIdCache.put(item.getDomain(), item.getId()); + // 更新端口映射到licenseId的缓存 + idToLicenseIdCache.put(item.getId(), item.getLicenseId()); + // 刷新流量限制缓存 + refreshFlowLimitCache(item.getId(), item.getUpLimitRate(), item.getDownLimitRate()); + }); + + // 未配置域名,则不需要处理域名映射逻辑 + if (StrUtil.isBlank(proxyConfig.getServer().getTcp().getDomainName())) { + return; + } + List portMappingDOList = allMappingDOList.stream() + .filter(item -> NetworkProtocolEnum.HTTP.getDesc().equals(item.getProtocal()) && item.getDomain() != null) + .collect(Collectors.toList()); + if (CollectionUtil.isEmpty(portMappingDOList)) { + return; + } + portMappingDOList.forEach(item -> { + if (StrUtil.isBlank(item.getSubdomain())) { + return; + } + ProxyUtil.setSubdomainToServerPort(item.getSubdomain(), item.getServerPort()); + + }); + } + + /** + * 刷新流量限制缓存 + * @param id + * @param upLimitRate + * @param downLimitRate + */ + private void refreshFlowLimitCache(Integer id, String upLimitRate, String downLimitRate) { + if (null == id) { + return; + } + flowLimitCache.put(id, new FlowLimitBO() + .setUpLimitRate(StringUtil.parseBytes(upLimitRate)) + .setDownLimitRate(StringUtil.parseBytes(downLimitRate)) + ); + } + + /** + * 获取license的流量限制 + * @param id + * @return + */ + public FlowLimitBO getFlowLimit(Integer id) { + FlowLimitBO res = flowLimitCache.get(id); + if (null == res) { + DomainMappingDO domainMappingDO = domainMappingMapper.findById(id); + if (null != domainMappingDO) { + refreshFlowLimitCache(id, domainMappingDO.getUpLimitRate(), domainMappingDO.getDownLimitRate()); + res = flowLimitCache.get(id); + } + } + return res; + } + + public Integer getDomainMappingIdByServerPort(Integer serverPort) { + if (null == serverPort) { + return null; + } + Integer id = domainToDomainMappingIdCache.get(serverPort); + if (null != id) { + return id; + } + List domainMappingDOList = domainMappingMapper.findListByServerPort(serverPort); + // 不存在 或者 有多条记录,都不处理 + if (CollectionUtils.isEmpty(domainMappingDOList) || domainMappingDOList.size() > 1) { + return null; + } + id = domainMappingDOList.get(0).getId(); + domainToDomainMappingIdCache.put(serverPort, id); + return id; + } + + public Integer getLicenseIdById(Integer id) { + Integer licenseId = idToLicenseIdCache.get(id); + if (null == licenseId) { + DomainMappingDO domainMappingDO = domainMappingMapper.findById(id); + if (null != domainMappingDO) { + licenseId = domainMappingDO.getLicenseId(); + idToLicenseIdCache.put(id, licenseId); + } + } + return licenseId; + } + + public FlowLimitBO getFlowLimitByServerPort(Integer serverPort) { + Integer id = getPortMappingIdByServerPort(serverPort); + if (null == id) { + return null; + } + FlowLimitBO res = getFlowLimit(id); + if (null == res || (null == res.getUpLimitRate() && null == res.getDownLimitRate())) { + Integer licenseId = getLicenseIdById(id); + if (null != licenseId) { + res = licenseService.getFlowLimit(licenseId); + } + } + return res; + } + + private void updateMappingDomainToSecurityGroupMap(Integer serverPort, Integer securityGroupId) { + if (securityGroupId == null || securityGroupId == 0) { + mappingDomainToSecurityGroupMap.remove(serverPort); + return; + } + mappingDomainToSecurityGroupMap.put(serverPort, securityGroupId); + } + + @Override + public void start() throws Throwable { + + } + + /** + * 服务端项目停止、启动时,更新在线状态为离线 + */ + @Override + public void stop() throws Throwable { + domainMappingMapper.updateOnlineStatus(OnlineStatusEnum.OFFLINE.getStatus(), new Date()); + } + + public DomainMappingDto one(Integer id) { + DomainMappingDO domainMappingDO = domainMappingMapper.findById(id); + if (null == domainMappingDO) { + return null; + } + DomainMappingDto res = (DomainMappingDto)domainMappingDO; + + LicenseDO license = licenseMapper.findById(domainMappingDO.getLicenseId()); + if (null != license) { + res.setLicenseName(license.getName()); + res.setUserId(license.getUserId()); + UserDO user = userMapper.findById(license.getUserId()); + if (null != user) { + res.setUserName(user.getName()); + } + } + return res; + } +} diff --git a/neutrino-proxy-server/src/main/resources/mapper/DomainMappingMapper.xml b/neutrino-proxy-server/src/main/resources/mapper/DomainMappingMapper.xml new file mode 100644 index 00000000..8d169bdd --- /dev/null +++ b/neutrino-proxy-server/src/main/resources/mapper/DomainMappingMapper.xml @@ -0,0 +1,34 @@ + + + + + + \ No newline at end of file diff --git a/neutrino-proxy-server/src/main/resources/mapper/PortMappingMapper.xml b/neutrino-proxy-server/src/main/resources/mapper/PortMappingMapper.xml index 22348ef9..72b473d0 100644 --- a/neutrino-proxy-server/src/main/resources/mapper/PortMappingMapper.xml +++ b/neutrino-proxy-server/src/main/resources/mapper/PortMappingMapper.xml @@ -2,7 +2,7 @@ - SELECT pm.* from port_mapping pm left join license on pm.license_id = license.`id`