diff --git a/docs/如何贡献.MD b/docs/如何贡献.MD index 46c8028c..e923e182 100644 --- a/docs/如何贡献.MD +++ b/docs/如何贡献.MD @@ -31,7 +31,7 @@ - 原则上,不允许自行添加第三方依赖库。若确有必要,请先和项目负责人沟通达成一致。 - 对现有代码的优化,请在注释中说明原因。 - 如果涉及到数据库的变更,则至少需要做到以下绩点: - - sqlite、mysql下均测试通过 + - h2、mysql下均测试通过 - 在`./neutrino-proxy-server/src/main/resource/sql`下维护更新对应的表结构sql文件 - 对于新增的表,维护必要的初始化数据 - - 在对应的mysql、sqlite的update目录下,维护SQL变更记录,确保SQL执行测试通过 \ No newline at end of file + - 在对应的mysql、h2的update目录下,维护SQL变更记录,确保SQL执行测试通过 \ No newline at end of file diff --git a/docs/快速使用.MD b/docs/快速使用.MD index 386ff53e..e444b3a4 100644 --- a/docs/快速使用.MD +++ b/docs/快速使用.MD @@ -37,7 +37,7 @@ registry.cn-hangzhou.aliyuncs.com/asgc/neutrino-proxy:1.7.1 - 在服务器上新建部署目录:`/work/projects/neutrino-proxy-server` - 将` neutrino-proxy-server.jar`、`neutrino-proxy-admin.zip`上传至服务器部署目录。 - 解压`neutrino-proxy-admin.zip`文件 -- 执行命令`java -jar neutrino-proxy-server.jar`启动服务端完成部署,默认使用sqlite数据库。 +- 执行命令`java -jar neutrino-proxy-server.jar`启动服务端完成部署,默认使用h2数据库。 - 若需要指定自己的mysql数据库,同样的需要在当前目录下新建`app.yml`文件,文件内容同上。执行命令`java -jar neutrino-proxy-server.jar config=app.yml`启动服务端完成部署 - 可参照 https://gitee.com/dromara/neutrino-proxy/blob/master/bin/server_start.sh 使用shell脚本启动服务端。 diff --git a/docs/管理后台使用指南.MD b/docs/管理后台使用指南.MD index b4762dbc..f4399fde 100644 --- a/docs/管理后台使用指南.MD +++ b/docs/管理后台使用指南.MD @@ -8,6 +8,7 @@ # 代理配置 - License管理:License是客户端连接服务端的唯一合法凭证。一个License同时只能被一个客户端使用,一个License可以维护多条端口映射 - 端口映射:服务端IP+端口 -> 客户端IP+端口的四元组映射(因目前服务端单节点只有一个公网IP,所以不体现出来),是内网穿透的基本单元。 +- 域名管理:用于统一管理服务器主域名和证书,支持多域名。 # 系统管理 - 用户管理:支持多用户,一个用户可持有多个License。由于项目目前主推个人版,所以暂是没有权限这一套,管理员之外的所有用户都属于游客。对于绝大多数操作,游客仅有只读权限。 diff --git a/neutrino-proxy-admin/src/api/domain.js b/neutrino-proxy-admin/src/api/domain.js index d3a9f7fd..5bad56b0 100644 --- a/neutrino-proxy-admin/src/api/domain.js +++ b/neutrino-proxy-admin/src/api/domain.js @@ -7,3 +7,69 @@ export function domainNameBindInfo(query) { params: query }) } +export function fetchList(query) { + return request({ + url: '/domain/page', + method: 'get', + params: query + }) +} + +export function fetchAvailableDomainList(query) { + return request({ + url: '/domain/all', + method: 'get', + params: query + }) +} + +export function updateEnableStatus(id, enable, domain) { + return request({ + url: '/domain/update/enable-status', + method: 'post', + data: { + id: id, + enable: enable, + domain: domain, + } + }) +} + +export function createDomain(data) { + return request({ + url: '/domain/create', + method: 'post', + data + }) +} + +export function updateDomain(data) { + return request({ + url: '/domain/update', + method: 'post', + data + }) +} + +export function deleteDomain(id) { + return request({ + url: '/domain/delete', + method: 'post', + params: { + id: id + } + }) +} + +export function updateDefaultStatus(id, isDefault) { + return request({ + url: '/domain/update/default-status', + method: 'post', + data: { + id: id, + isDefault: isDefault + } + }) +} + + diff --git a/neutrino-proxy-admin/src/lang/zh.js b/neutrino-proxy-admin/src/lang/zh.js index 998795df..f57909e7 100644 --- a/neutrino-proxy-admin/src/lang/zh.js +++ b/neutrino-proxy-admin/src/lang/zh.js @@ -55,6 +55,7 @@ export default { proxy: '代理配置', license: 'License管理', portMapping: '端口映射', + domainManagement: '域名管理', jobManager: '调度管理', jobLog: '调度日志', log: '日志管理', @@ -128,6 +129,7 @@ export default { deleteBatch: '批量删除', cancel: '取 消', confirm: '确 定', + domain: '域名', userId: '用户ID', userName: '用户名', name: '名称', diff --git a/neutrino-proxy-admin/src/router/index.js b/neutrino-proxy-admin/src/router/index.js index 47a870f2..8b2b4a87 100644 --- a/neutrino-proxy-admin/src/router/index.js +++ b/neutrino-proxy-admin/src/router/index.js @@ -61,7 +61,8 @@ export const asyncRouterMap = [ }, children: [ { path: 'license', component: _import('proxy/license'), name: 'license', meta: { title: 'license' }}, - { path: 'portMapping', component: _import('proxy/portMapping'), name: 'portMapping', meta: { title: 'portMapping' }} + { path: 'portMapping', component: _import('proxy/portMapping'), name: 'portMapping', meta: { title: 'portMapping' }}, + { path: 'domainManagement', component: _import('proxy/domain'), name: 'domainManagement', meta: { title: 'domainManagement' }} ] }, { @@ -78,7 +79,7 @@ export const asyncRouterMap = [ { path: 'portGroup', component: _import('system/portGroup'), name: 'portGroup', meta: { title: 'portGroup' }}, { path: 'portPool', component: _import('system/portPool'), name: 'portPool', meta: { title: 'portPool' }}, { path: 'securityGroup', component: _import('system/securityGroup'), name: 'securityGroup', meta: { title: 'securityGroup' }}, - { path: 'securityRule', component: _import('system/securityRule'), name: 'securityRule', meta: { title: 'securityRule' }, hidden: true}, + { path: 'securityRule', component: _import('system/securityRule'), name: 'securityRule', meta: { title: 'securityRule' }, hidden: true }, { path: 'protocal', component: _import('system/protocal'), name: 'protocal', meta: { title: 'protocal' }}, { path: 'jobManager', component: _import('system/jobManager'), name: 'jobManager', meta: { title: 'jobManager' }} ] diff --git a/neutrino-proxy-admin/src/views/proxy/domain.vue b/neutrino-proxy-admin/src/views/proxy/domain.vue new file mode 100644 index 00000000..840c5fd3 --- /dev/null +++ b/neutrino-proxy-admin/src/views/proxy/domain.vue @@ -0,0 +1,467 @@ + + + diff --git a/neutrino-proxy-admin/src/views/proxy/license.vue b/neutrino-proxy-admin/src/views/proxy/license.vue index 8cc70379..adb92f2b 100644 --- a/neutrino-proxy-admin/src/views/proxy/license.vue +++ b/neutrino-proxy-admin/src/views/proxy/license.vue @@ -1,13 +1,13 @@ - + @@ -167,10 +169,18 @@ - - - - + +
+ + + +
+ 添加域名
@@ -225,13 +235,13 @@ import { availablePortList, portAvailable } from '@/api/portPool' import { licenseList, licenseAuthList } from '@/api/license' import { protocalList } from '@/api/protocal' import { userList } from '@/api/user' -import { domainNameBindInfo } from '@/api/domain' import waves from '@/directive/waves' // 水波纹指令 import { parseTime } from '@/utils' import ButtonPopover from '../../components/Button/buttonPopover' import DropdownTable from '../../components/Dropdown/DropdownTable' // 下拉选择加载组件 import loadSelect from "@/components/Select/SelectLoadMore"; +import {fetchAvailableDomainList} from "../../api/domain"; const calendarTypeOptions = [ { key: 'CN', display_name: 'China' }, @@ -297,7 +307,7 @@ export default { protocalList: [], licenseAuthList: [], serverPortList: [], - domainName: '', + domainList: [{ 'name': 'link.com', 'id': '1' }], showReviewer: false, temp: { id: undefined, @@ -308,7 +318,8 @@ export default { clientPort: undefined, protocal: undefined, proxyResponses: undefined, - proxyTimeoutMs: undefined + proxyTimeoutMs: undefined, + domainMappings: [] }, selectObj: { statusOptions: [{ label: '启用', value: 1 }, { label: '禁用', value: 2 }], @@ -323,6 +334,21 @@ export default { dialogPvVisible: false, pvData: [], rules: { + domainMappings: [ + { + validator: (rule, value, callback) => { + if (value && value.length > 0) { + for (let i = 0; i < value.length; i++) { + if (!value[i].subdomain || !value[i].domainId) { + return callback(new Error(`子域名和主域名都不能为空`)); + } + } + } + callback(); // 验证通过 + }, + trigger: '' + } + ], licenseId: [{ required: true, message: '请选择License', trigger: 'blur,change' }], serverPort: [{ required: true, message: '请输入服务端端口', trigger: 'blur' }, // { validator: isPortAvailable, trigger: 'change' } @@ -372,18 +398,64 @@ export default { } }, created() { - this.getDomainNameBindInfo() this.getDataList() this.getLicenseList() this.getLicenseAuthList() this.getProtocalList() this.fetchSecurityGroupList() + this.getAvailableDomainList() }, methods: { + // 添加新的域名映射 + addDomainMapping() { + let defaultDomainId = this.domainList[0].id + this.domainList.forEach(item => { + if (item.isDefault === 1) { + defaultDomainId = item.id + } + }) + this.temp.domainMappings.push({ + subdomain: undefined, + domain: undefined, + domainId: defaultDomainId + }); + console.log(this.list) + }, + // 删除指定的域名映射 + removeDomainMapping(index) { + this.temp.domainMappings.splice(index, 1); + }, getList() { this.listLoading = true fetchList(this.listQuery).then(response => { - this.list = response.data.data.records + this.list = response.data.data.records.map(item => { + // 初始化 domainMappings 字段 + item.domainMappings = []; + // 检查是否存在 subdomain 和 domain 字段 + if (item.fullDomainMappings && Array.isArray(item.fullDomainMappings)) { + let allFullDomainList = [] + for (const fullDomainMapping of item.fullDomainMappings) { + item.domainMappings.push({ + id: fullDomainMapping.id || '', + subdomain: fullDomainMapping.subdomain || '', // 使用 || '' 保证字段非空 + domainId: fullDomainMapping.domainNameId || '', + domain: fullDomainMapping.domain || '' + }); + // 拼接完整的域名 + let fullDomain = null; + if (fullDomainMapping.forceHttps == 1) { + fullDomain = `https://` + `${fullDomainMapping.subdomain}.${fullDomainMapping.domain}`; + } else { + fullDomain = `http://` + `${fullDomainMapping.subdomain}.${fullDomainMapping.domain}`; + } + // 将拼接后的域名添加到 + allFullDomainList.push(fullDomain); + } + item.allFullDomainList = allFullDomainList + } + return item; + }); + // console.log(this.list) this.total = response.data.data.total this.listLoading = false }) @@ -406,15 +478,15 @@ export default { } }) }, - getDomainNameBindInfo() { - domainNameBindInfo().then(response => { - this.domainName = response.data.data - }) - }, getAvailablePortList(licenseId) { this.loadServerPortQuery.licenseId = licenseId; this.serverPortList = []; //清掉数据 }, + getAvailableDomainList() { + fetchAvailableDomainList({}).then(response => { + this.domainList = response.data.data + }) + }, getAllUserList() { userList().then(response => { this.userList = response.data.data @@ -469,9 +541,10 @@ export default { clientPort: undefined, userId: undefined, proxyResponses: undefined, - proxyTimeoutMs: undefined - } - this.serverPortList = [] + proxyTimeoutMs: undefined, + domainMappings: [] + }; + this.serverPortList = []; this.loadServerPortQuery.licenseId = null; this.more = true; }, @@ -486,6 +559,7 @@ export default { createData() { this.$refs['dataForm'].validate((valid) => { if (valid) { + // console.log(this.temp) createUserPortMapping(this.temp).then(response => { if (response.data.code === 0) { this.dialogFormVisible = false @@ -503,13 +577,16 @@ export default { }, handleOpenWebPage(row) { let url = location.protocol + '//' + location.hostname + ':' + row.serverPort - if (row.domain) { - url = location.protocol + '//' + row.domain + if (row.allFullDomainList && row.allFullDomainList.length > 0) { + url = row.allFullDomainList[0] } open(url) }, handleUpdate(row) { - this.temp = Object.assign({}, row) // copy obj + this.resetTemp() + console.log(this.temp) + this.temp = JSON.parse(JSON.stringify(row)) + console.log(this.temp) if (row.securityGroupId === 0) { this.temp.securityGroupId = null } diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/base/NeutrinoServerRuntimeNativeRegistrar.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/base/NeutrinoServerRuntimeNativeRegistrar.java index 19306ba2..702812e2 100644 --- a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/base/NeutrinoServerRuntimeNativeRegistrar.java +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/base/NeutrinoServerRuntimeNativeRegistrar.java @@ -3,8 +3,11 @@ package org.dromara.neutrinoproxy.server.base; import org.apache.ibatis.type.SimpleTypeRegistry; import org.dromara.neutrinoproxy.server.base.proxy.ProxyConfig; import org.dromara.neutrinoproxy.server.base.rest.ResponseBody; +import org.dromara.neutrinoproxy.server.controller.req.proxy.PortMappingCreateReq; +import org.dromara.neutrinoproxy.server.controller.req.proxy.PortMappingUpdateReq; import org.dromara.neutrinoproxy.server.controller.res.report.HomeDataView; import org.dromara.neutrinoproxy.server.dal.*; +import org.dromara.neutrinoproxy.server.proxy.enhance.SslContextManager; import org.dromara.neutrinoproxy.server.service.*; import org.dromara.solonplugins.job.JobBean; import org.noear.solon.annotation.Component; @@ -38,7 +41,8 @@ public class NeutrinoServerRuntimeNativeRegistrar implements RuntimeNativeRegist metadata.registerLambdaSerialization(UserLoginRecordService.class); metadata.registerLambdaSerialization(UserService.class); metadata.registerLambdaSerialization(SecurityGroupService.class); - + metadata.registerLambdaSerialization(DomainService.class); + metadata.registerLambdaSerialization(SslContextManager.class); metadata.registerLambdaSerialization(LicenseMapper.class); metadata.registerLambdaSerialization(ClientConnectRecordMapper.class); @@ -57,7 +61,11 @@ public class NeutrinoServerRuntimeNativeRegistrar implements RuntimeNativeRegist metadata.registerLambdaSerialization(UserTokenMapper.class); metadata.registerLambdaSerialization(SecurityGroupMapper.class); metadata.registerLambdaSerialization(SecurityRuleMapper.class); + metadata.registerLambdaSerialization(DomainMapper.class); + metadata.registerLambdaSerialization(DomainPortMappingMapper.class); + metadata.registerReflection(PortMappingUpdateReq.DomainMapping.class, MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS); + metadata.registerReflection(PortMappingCreateReq.DomainMapping.class, MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS); metadata.registerReflection(HomeDataView.class, MemberCategory.DECLARED_FIELDS); metadata.registerReflection(HomeDataView.Last7dFlow.class, MemberCategory.DECLARED_FIELDS); diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/base/proxy/ProxyConfig.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/base/proxy/ProxyConfig.java index 60f89e23..eb48ca3b 100644 --- a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/base/proxy/ProxyConfig.java +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/base/proxy/ProxyConfig.java @@ -63,7 +63,6 @@ public class ProxyConfig { public static class Tcp { private Integer bossThreadCount; private Integer workThreadCount; - private String domainName; private Integer httpProxyPort; private Integer httpsProxyPort; private String keyStorePassword; diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/constant/DefaultDomainStatusEnum.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/constant/DefaultDomainStatusEnum.java new file mode 100644 index 00000000..65608f78 --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/constant/DefaultDomainStatusEnum.java @@ -0,0 +1,19 @@ +package org.dromara.neutrinoproxy.server.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 默认域名状态枚举 + * @author Mirac + * @date 21/7/2024 + */ +@Getter +@AllArgsConstructor +public enum DefaultDomainStatusEnum { + ENABLE(1, "默认域名"), + DISABLE(2, "关闭"); + + private Integer status; + private String desc; +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/constant/ExceptionConstant.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/constant/ExceptionConstant.java index ed149c80..7bdca2a0 100644 --- a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/constant/ExceptionConstant.java +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/constant/ExceptionConstant.java @@ -59,8 +59,6 @@ public enum ExceptionConstant { PORT_MAPPING_NOT_EXIST(14000, "端口映射记录不存在"), PORT_CANNOT_REPEAT_MAPPING(14001, "服务端口[{}]不能重复映射"), AN_UNSUPPORTED_PROTOCOL(14002, "不支持的协议[{}]!"), - PORT_MAPPING_SUBDONAME_CONNOT_REPEAT(14003, "子域名不能重复使用!"), - // 调度管理(15000) JOB_INFO_NOT_EXIST(15000, "调度管理记录不存在"), SYSTEM_ERROR(500, "系统异常"), @@ -72,7 +70,12 @@ public enum ExceptionConstant { // 安全组管理(17000) SECURITY_GROUP_NOT_EXIST(17000, "安全组不存在"), SECURITY_RULE_NOT_EXIST(17001, "安全规则不存在"), - + // 域名映射管理(18000) + DOMAIN_NAME_CANNOT_REPEAT(18000, "域名不能重复"), + DOMAIN_NAME_NOT_EXIST(18001, "域名不存在"), + DOMAIN_NAME_IS_DISABLE(18002, "当前域名[{}]被禁用"), + SUDOMAIN_NAME_CANNOT_REPEAT(18003, "子域名不能重复"), + DOMAIN_NAME_IS_USED(18002, "当前域名正在使用"), ; private int code; diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/constant/HttpsStatusEnum.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/constant/HttpsStatusEnum.java new file mode 100644 index 00000000..ad25a003 --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/constant/HttpsStatusEnum.java @@ -0,0 +1,29 @@ +package org.dromara.neutrinoproxy.server.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 仅Https状态枚举 + * @author Mirac + * @date 21/7/2024 + */ +@Getter +@AllArgsConstructor +public enum HttpsStatusEnum { + ONLY_HTTPS(1, "仅HTTPS"), + DISABLE_ONLY_HTTPS(2, "关闭仅HTTPS"); + private static Map CACHE = Stream.of(HttpsStatusEnum.values()).collect(Collectors.toMap(HttpsStatusEnum::getStatus, Function.identity())); + + private Integer status; + private String desc; + + public static HttpsStatusEnum of(Integer status) { + return CACHE.get(status); + } +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/constant/SslStatusEnum.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/constant/SslStatusEnum.java new file mode 100644 index 00000000..4664a1e2 --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/constant/SslStatusEnum.java @@ -0,0 +1,25 @@ +package org.dromara.neutrinoproxy.server.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 仅Https状态枚举 + * @author Mirac + * @date 21/7/2024 + */ +@Getter +@AllArgsConstructor +public enum SslStatusEnum { + UPLOADED(1, "已上传"), + NOT_UPLOADED(2, "未上传"), + CERTIFIED(3, "已认证");//预留,用于证书验证 + + private Integer status; + private String desc; +} 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..cef5f3f3 --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/DomainController.java @@ -0,0 +1,91 @@ +package org.dromara.neutrinoproxy.server.controller; + +import cn.hutool.core.util.StrUtil; +import org.dromara.neutrinoproxy.server.base.page.PageInfo; +import org.dromara.neutrinoproxy.server.base.page.PageQuery; +import org.dromara.neutrinoproxy.server.base.rest.Authorization; +import org.dromara.neutrinoproxy.server.controller.req.proxy.*; +import org.dromara.neutrinoproxy.server.controller.res.proxy.DomainListRes; +import org.dromara.neutrinoproxy.server.controller.res.proxy.DomainUpdateDefaultStatusRes; +import org.dromara.neutrinoproxy.server.controller.res.proxy.DomainUpdateEnableStatusRes; +import org.dromara.neutrinoproxy.server.util.ParamCheckUtil; +import org.noear.solon.annotation.*; +import org.dromara.neutrinoproxy.server.service.DomainService; +import org.noear.solon.core.handle.Context; +import org.noear.solon.core.handle.UploadedFile; +import org.noear.solon.lang.Nullable; + +import javax.sound.midi.Soundbank; +import java.io.IOException; +import java.util.List; + +/** + * @author Mirac + * @date 21/7/2024 + */ +@Mapping("/domain") +@Controller +public class DomainController { + @Inject + private DomainService domainService; + + @Get + @Mapping("/page") + public PageInfo page(PageQuery pageQuery, DomainListReq req) { + return domainService.page(pageQuery, req); + } + @Get + @Mapping("/all") + public List all(DomainListReq req) { + ParamCheckUtil.checkNotNull(req, "req"); + return domainService.all(req); + } + + @Post + @Mapping(path = "/create") + @Authorization(onlyAdmin = true) + public void create(DomainCreateReq req, UploadedFile jks) throws IOException { + ParamCheckUtil.checkNotNull(req, "req"); + ParamCheckUtil.checkNotEmpty(req.getDomain(), "domain"); + domainService.create(req, jks); + } + + @Post + @Mapping(path = "/update") + @Authorization(onlyAdmin = true) + public void update(DomainUpdateReq req, UploadedFile jks) throws IOException { + ParamCheckUtil.checkNotNull(req, "req"); + ParamCheckUtil.checkNotEmpty(req.getDomain(), "domain"); + domainService.update(req, jks); + } + + @Post + @Mapping(path = "/update/enable-status") + @Authorization(onlyAdmin = true) + public DomainUpdateEnableStatusRes updateEnableStatus(DomainUpdateEnableStatusReq req){ + ParamCheckUtil.checkNotNull(req, "req"); + ParamCheckUtil.checkNotNull(req.getId(), "id"); + ParamCheckUtil.checkNotNull(req.getEnable(), "enable"); + return domainService.updateEnableStatus(req); + } + + @Post + @Mapping(path = "/delete") + @Authorization(onlyAdmin = true) + public void delete(DomainDeleteReq req) { + ParamCheckUtil.checkNotNull(req, "req"); + ParamCheckUtil.checkNotNull(req.getId(), "id"); + + domainService.delete(req.getId()); + } + + @Post + @Mapping(path = "/update/default-status") + @Authorization(onlyAdmin = true) + public DomainUpdateDefaultStatusRes updateDefaultStatus(DomainUpdateDefaultStatusReq req){ + ParamCheckUtil.checkNotNull(req, "req"); + ParamCheckUtil.checkNotNull(req.getId(), "id"); + ParamCheckUtil.checkNotNull(req.getIsDefault(), "isDefault"); + return domainService.updateDefaultStatus(req.getId(), req.getIsDefault()); + } +} 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 3f64d661..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-name") -@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/PortMappingController.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/PortMappingController.java index ea1dcbe5..dcfb74b1 100644 --- a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/PortMappingController.java +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/PortMappingController.java @@ -30,7 +30,6 @@ public class PortMappingController { @Mapping("/page") public PageInfo page(PageQuery pageQuery, PortMappingListReq req) { ParamCheckUtil.checkNotNull(pageQuery, "pageQuery"); - return portMappingService.page(pageQuery, req); } @@ -53,7 +52,7 @@ public class PortMappingController { ParamCheckUtil.checkNotNull(networkProtocolEnum, ExceptionConstant.AN_UNSUPPORTED_PROTOCOL, req.getProtocal()); if (networkProtocolEnum != NetworkProtocolEnum.HTTP) { // 目前仅HTTP支持绑定域名 - req.setSubdomain(null); + req.setDomainMappings(null); } req.setProtocal(networkProtocolEnum.getDesc()); if (null == req.getProxyResponses()) { @@ -88,7 +87,7 @@ public class PortMappingController { ParamCheckUtil.checkNotNull(networkProtocolEnum, ExceptionConstant.AN_UNSUPPORTED_PROTOCOL, req.getProtocal()); if (networkProtocolEnum != NetworkProtocolEnum.HTTP) { // 目前仅HTTP支持绑定域名 - req.setSubdomain(null); + req.setDomainMappings(null); } req.setProtocal(networkProtocolEnum.getDesc()); if (null == req.getProxyResponses()) { diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/DomainCreateReq.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/DomainCreateReq.java new file mode 100644 index 00000000..fe30910d --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/DomainCreateReq.java @@ -0,0 +1,34 @@ +package org.dromara.neutrinoproxy.server.controller.req.proxy; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Data; +import org.dromara.neutrinoproxy.server.constant.DefaultDomainStatusEnum; +import org.dromara.neutrinoproxy.server.constant.EnableStatusEnum; +import org.dromara.neutrinoproxy.server.constant.HttpsStatusEnum; +import org.noear.solon.core.handle.UploadedFile; +import org.noear.solon.lang.Nullable; + +import java.util.Date; + +/** + * @author Mirac + * @date 21/7/2024 + */ +@Data +public class DomainCreateReq { + /** + * 主域名 + */ + private String domain; + + /** + * KeyStore密码 + */ + private String keyStorePassword; + + /** + * 强制Https + */ + private Integer forceHttps; +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/DomainDeleteReq.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/DomainDeleteReq.java new file mode 100644 index 00000000..a6716739 --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/DomainDeleteReq.java @@ -0,0 +1,15 @@ +package org.dromara.neutrinoproxy.server.controller.req.proxy; + +import lombok.Data; + +/** + * @author Mirac + * @date 23/7/2024 + */ +@Data +public class DomainDeleteReq { + /** + * id + */ + private Integer id; +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/DomainListReq.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/DomainListReq.java new file mode 100644 index 00000000..e4f6964a --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/DomainListReq.java @@ -0,0 +1,21 @@ +package org.dromara.neutrinoproxy.server.controller.req.proxy; + +import lombok.Data; +import org.dromara.neutrinoproxy.server.constant.EnableStatusEnum; + +/** + * @author Mirac + * @date 21/7/2024 + */ +@Data +public class DomainListReq { + /** + * 用户ID + */ + private Integer userId; + /** + * 启用状态 + * {@link EnableStatusEnum} + */ + private Integer enable; +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/DomainUpdateDefaultStatusReq.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/DomainUpdateDefaultStatusReq.java new file mode 100644 index 00000000..a0be7014 --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/DomainUpdateDefaultStatusReq.java @@ -0,0 +1,22 @@ +package org.dromara.neutrinoproxy.server.controller.req.proxy; + +import lombok.Data; +import org.dromara.neutrinoproxy.server.constant.DefaultDomainStatusEnum; + +/** + * @author Mirac + * @date 23/7/2024 + */ +@Data +public class DomainUpdateDefaultStatusReq { + + /** + * id + */ + private Integer id; + + /** + * 启用状态 {@link DefaultDomainStatusEnum} + */ + private Integer isDefault; +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/DomainUpdateEnableStatusReq.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/DomainUpdateEnableStatusReq.java new file mode 100644 index 00000000..1ac3e549 --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/DomainUpdateEnableStatusReq.java @@ -0,0 +1,26 @@ +package org.dromara.neutrinoproxy.server.controller.req.proxy; + +import lombok.Data; + +/** + * @author Mirac + * @date 23/7/2024 + */ +@Data +public class DomainUpdateEnableStatusReq { + + /** + * id + */ + private Integer id; + + /** + * 启用状态 + */ + private Integer enable; + + /** + * 域名 + */ + private String domain; +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/DomainUpdateReq.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/DomainUpdateReq.java new file mode 100644 index 00000000..fdcd3775 --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/DomainUpdateReq.java @@ -0,0 +1,30 @@ +package org.dromara.neutrinoproxy.server.controller.req.proxy; + +import lombok.Data; + +/** + * @author Mirac + * @date 21/7/2024 + */ +@Data +public class DomainUpdateReq { + /** + * id + */ + private Integer id; + + /** + * 主域名 + */ + private String domain; + + /** + * KeyStore密码 + */ + private String keyStorePassword; + + /** + * 强制Https + */ + private Integer forceHttps; +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/PortMappingCreateReq.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/PortMappingCreateReq.java index 87ae362d..c701771c 100644 --- a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/PortMappingCreateReq.java +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/PortMappingCreateReq.java @@ -23,6 +23,8 @@ package org.dromara.neutrinoproxy.server.controller.req.proxy; import lombok.Data; +import java.util.List; + /** * 端口映射创建请求 * @author: aoshiguchen @@ -38,10 +40,6 @@ public class PortMappingCreateReq { * 协议 */ private String protocal; - /** - * 子域名 - */ - private String subdomain; /** * 服务端端口 */ @@ -80,4 +78,15 @@ public class PortMappingCreateReq { * 描述 */ private String description; + + /** + * 域名映射列表 + */ + private List domainMappings; + + @Data + public class DomainMapping { + private Integer domainId; + private String subdomain; + } } diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/PortMappingUpdateReq.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/PortMappingUpdateReq.java index 31f85a01..6ddea989 100644 --- a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/PortMappingUpdateReq.java +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/req/proxy/PortMappingUpdateReq.java @@ -23,6 +23,8 @@ package org.dromara.neutrinoproxy.server.controller.req.proxy; import lombok.Data; +import java.util.List; + /** * 端口映射更新请求 * @author: aoshiguchen @@ -42,10 +44,6 @@ public class PortMappingUpdateReq { * 协议 */ private String protocal; - /** - * 子域名 - */ - private String subdomain; /** * 服务端端口 */ @@ -84,4 +82,16 @@ public class PortMappingUpdateReq { * 描述 */ private String description; + + /** + * 域名映射列表 + */ + private List domainMappings; + + @Data + public class DomainMapping { + private Integer id; + private Integer domainId; + private String subdomain; + } } diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/res/proxy/DomainListRes.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/res/proxy/DomainListRes.java new file mode 100644 index 00000000..12d5ecd5 --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/res/proxy/DomainListRes.java @@ -0,0 +1,72 @@ +package org.dromara.neutrinoproxy.server.controller.res.proxy; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dromara.neutrinoproxy.server.constant.DefaultDomainStatusEnum; +import org.dromara.neutrinoproxy.server.constant.EnableStatusEnum; +import org.dromara.neutrinoproxy.server.constant.HttpsStatusEnum; +import org.dromara.neutrinoproxy.server.constant.SslStatusEnum; + +import java.util.Date; + +/** + * @author Mirac + * @date 21/7/2024 + */ +@Accessors(chain = true) +@Data +public class DomainListRes { + @TableId(type = IdType.AUTO) + private Integer id; + + /** + * 主域名 + */ + private String domain; + + /** + * 用户ID + */ + private Integer userId; + + /** + * 用户名 + */ + private String userName; + + /** + * 是否是默认域名(1、是 2、否) + * {@link DefaultDomainStatusEnum} + */ + private Integer isDefault; + + /** + * 强制使用HTTPS(1、是 2、否) + * {@link HttpsStatusEnum} + */ + private Integer forceHttps; + + /** + * 启用状态 + * {@link EnableStatusEnum} + */ + private Integer enable; + + /** + * SSLStatusEnum + * {@link SslStatusEnum} + */ + private Integer sslStatus; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新时间 + */ + private Date updateTime; +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/res/proxy/DomainUpdateDefaultStatusRes.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/res/proxy/DomainUpdateDefaultStatusRes.java new file mode 100644 index 00000000..b4c55aa8 --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/res/proxy/DomainUpdateDefaultStatusRes.java @@ -0,0 +1,8 @@ +package org.dromara.neutrinoproxy.server.controller.res.proxy; + +/** + * @author Mirac + * @date 23/7/2024 + */ +public class DomainUpdateDefaultStatusRes { +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/res/proxy/DomainUpdateEnableStatusRes.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/res/proxy/DomainUpdateEnableStatusRes.java new file mode 100644 index 00000000..f60f677e --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/res/proxy/DomainUpdateEnableStatusRes.java @@ -0,0 +1,8 @@ +package org.dromara.neutrinoproxy.server.controller.res.proxy; + +/** + * @author Mirac + * @date 23/7/2024 + */ +public class DomainUpdateEnableStatusRes { +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/res/proxy/PortMappingListRes.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/res/proxy/PortMappingListRes.java index 465f9a6d..e9d4a10c 100644 --- a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/res/proxy/PortMappingListRes.java +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/res/proxy/PortMappingListRes.java @@ -21,11 +21,13 @@ */ package org.dromara.neutrinoproxy.server.controller.res.proxy; +import lombok.Data; import org.dromara.neutrinoproxy.server.constant.EnableStatusEnum; import org.dromara.neutrinoproxy.server.constant.OnlineStatusEnum; -import lombok.Data; +import org.dromara.neutrinoproxy.server.service.bo.FullDomainNameBO; import java.util.Date; +import java.util.List; /** * 端口映射列表响应 @@ -43,14 +45,10 @@ public class PortMappingListRes { * 协议 */ private String protocal; - /** - * 子域名 - */ - private String subdomain; - /** - * 域名 - */ - private String domain; + /** + * 完整域名映射 + */ + private List fullDomainMappings; /** * license名称 */ diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/DomainMapper.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/DomainMapper.java new file mode 100644 index 00000000..89d49487 --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/DomainMapper.java @@ -0,0 +1,61 @@ +package org.dromara.neutrinoproxy.server.dal; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.dromara.neutrinoproxy.server.dal.entity.DomainNameDO; +import org.dromara.neutrinoproxy.server.service.bo.FullDomainNameBO; + +import java.util.Date; +import java.util.List; +import java.util.Set; + + +@Mapper +public interface DomainMapper extends BaseMapper { + default DomainNameDO checkRepeat(String domain, Set excludeIds) { + // 查询与输入域名相关的域名列表 + List domainList = this.selectList(new LambdaQueryWrapper() + .notIn(excludeIds != null, DomainNameDO::getId, excludeIds) + ); + + // 遍历数据库中的域名,检查是否存在重复(包括子域名) + for (DomainNameDO domainNameDO : domainList) { + String existingDomain = domainNameDO.getDomain(); + // 完全相等的情况 + if (existingDomain.equals(domain)) { + return domainNameDO; + } + // 是子域名关系 + if (isSubdomainOrEqual(existingDomain, domain)) { + return domainNameDO; + } + } + + // 如果没有找到重复的,返回 null + return null; + } + + // 辅助方法:判断 domain1 是否是 domain2 的子域名,或者两者相等 + private boolean isSubdomainOrEqual(String domain1, String domain2) { + // 判断是否是子域名关系,例如 "proxy.asgc.fun" 是 "asgc.fun" 的子域名 + return domain1.endsWith("." + domain2) || domain2.endsWith("." + domain1); + } + + default void updateEnableStatus(Integer id, Integer enable, Date updateTime) { + this.update(Wrappers.lambdaUpdate() + .eq(DomainNameDO::getId, id) + .set(DomainNameDO::getEnable, enable) + .set(DomainNameDO::getUpdateTime, updateTime) + ); + } + + + List selectFullDomainNameListByPortMappingIds(@Param("ids") Set ids); + + List selectFullDomainNameListByDomainNameIds(@Param("ids") Set ids); + + List selectFullDomainNameList(); +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/DomainPortMappingMapper.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/DomainPortMappingMapper.java new file mode 100644 index 00000000..6a5153ae --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/DomainPortMappingMapper.java @@ -0,0 +1,41 @@ +package org.dromara.neutrinoproxy.server.dal; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import org.apache.ibatis.annotations.Mapper; +import org.dromara.neutrinoproxy.server.dal.entity.DomainPortMappingDO; + +import java.util.List; + +/** + * @author: Mirac + * @date: 2024/8/24 + */ +@Mapper +public interface DomainPortMappingMapper extends BaseMapper { + default Boolean checkRepeatBySubdomain(String subdomain, Integer domainNameId, Integer id) { + return StringUtils.isEmpty(subdomain)? Boolean.FALSE : this.selectCount(new LambdaQueryWrapper() + .eq(DomainPortMappingDO::getSubdomain, subdomain) + .eq(DomainPortMappingDO::getDomainNameId, domainNameId) + .ne(id != null, DomainPortMappingDO::getId, id) + .last("limit 1") + ).intValue() > 0; + } + + default List findByPortMappingId(Integer portMappingId) { + return this.selectList(Wrappers.lambdaQuery() + .eq(DomainPortMappingDO::getPortMappingId, portMappingId)); + } + + default void deleteByDomainNameId(Integer doomainNameId) { + this.delete(Wrappers.lambdaQuery() + .eq(DomainPortMappingDO::getDomainNameId, doomainNameId)); + } + + default boolean checkUsed(Integer domainNameId) { + return this.exists(Wrappers.lambdaQuery() + .eq(DomainPortMappingDO::getDomainNameId, domainNameId)); + } +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/PortMappingMapper.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/PortMappingMapper.java index 71364325..4b02a106 100644 --- a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/PortMappingMapper.java +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/PortMappingMapper.java @@ -11,6 +11,7 @@ import org.dromara.neutrinoproxy.server.controller.req.proxy.PortMappingListReq; import org.dromara.neutrinoproxy.server.dal.entity.PortMappingDO; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +import org.dromara.neutrinoproxy.server.service.bo.FullDomainNameBO; import java.util.Date; import java.util.List; @@ -44,18 +45,6 @@ public interface PortMappingMapper extends BaseMapper { ); } - /** - * 校验子域名是否重复 - * @param subdomain - * @return - */ - default Boolean checkRepeatBySubdomain(String subdomain, Set excludeIds) { - return StringUtils.isEmpty(subdomain)? Boolean.FALSE : this.selectCount(new LambdaQueryWrapper() - .eq(PortMappingDO::getSubdomain, subdomain) - .notIn(!CollectionUtil.isEmpty(excludeIds), PortMappingDO::getId, excludeIds) - ).intValue() > 0; - } - default List findEnableListByLicenseId(Integer licenseId) { return this.selectList(new LambdaQueryWrapper() .eq(PortMappingDO::getLicenseId, licenseId) diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/entity/DomainNameDO.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/entity/DomainNameDO.java new file mode 100644 index 00000000..225430b2 --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/entity/DomainNameDO.java @@ -0,0 +1,104 @@ +/** + * 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.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.ToString; +import lombok.experimental.Accessors; +import org.dromara.neutrinoproxy.server.constant.*; +import org.dromara.neutrinoproxy.server.controller.res.proxy.DomainListRes; +import org.dromara.neutrinoproxy.server.controller.res.proxy.PortMappingListRes; + +import java.util.Date; + +/** + * 域名 + * @author: Mirac + * @date: 2024/7/21 + */ +@ToString +@Accessors(chain = true) +@Data +@TableName("domain_name") +public class DomainNameDO { + @TableId(type = IdType.AUTO) + private Integer id; + + /** + * 主域名 + */ + private String domain; + + /** + * 用户ID + */ + private Integer userId; + + /** + * SSL证书文件内容 + */ + private byte[] jks; + + /** + * KeyStore密码 + */ + private String keyStorePassword; + + /** + * 是否是默认域名(1、是 2、否) + * {@link DefaultDomainStatusEnum} + */ + private Integer isDefault; + + /** + * 强制使用HTTPS(1、是 2、否) + * {@link HttpsStatusEnum} + */ + private Integer forceHttps; + + /** + * 启用状态 + * {@link EnableStatusEnum} + */ + private Integer enable; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新时间 + */ + private Date updateTime; + + public DomainListRes toRes() { + DomainListRes res = new DomainListRes(); + BeanUtil.copyProperties(this, res); + res.setSslStatus(this.getJks() == null ? SslStatusEnum.NOT_UPLOADED.getStatus() : SslStatusEnum.UPLOADED.getStatus()); + return res; + } +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/entity/DomainPortMappingDO.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/entity/DomainPortMappingDO.java new file mode 100644 index 00000000..9ff8026e --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/entity/DomainPortMappingDO.java @@ -0,0 +1,64 @@ +/** + * 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.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.PortMappingListRes; + +import java.util.Date; + +/** + * @author: Mirac + * @date: 2024/8/24 + */ +@ToString +@Accessors(chain = true) +@Data +@TableName("domain_port_mapping") +public class DomainPortMappingDO { + + /** + * 主键 + */ + @TableId(type = IdType.AUTO) + private Integer id; + /** + * 端口映射id + */ + private Integer portMappingId; + /** + * 域名id + */ + private Integer domainNameId; + /** + * 子域名 + */ + private String subdomain; +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/entity/PortMappingDO.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/entity/PortMappingDO.java index 16745fda..75ba84a1 100644 --- a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/entity/PortMappingDO.java +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/entity/PortMappingDO.java @@ -54,10 +54,6 @@ public class PortMappingDO { * 协议 */ private String protocal; - /** - * 子域名 - */ - private String subdomain; /** * 服务端端口 */ diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/proxy/enhance/HttpProxy.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/proxy/enhance/HttpProxy.java index ce1197db..5a4c9a72 100644 --- a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/proxy/enhance/HttpProxy.java +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/proxy/enhance/HttpProxy.java @@ -1,8 +1,7 @@ package org.dromara.neutrinoproxy.server.proxy.enhance; -import cn.hutool.core.util.StrUtil; import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.*; +import io.netty.channel.ChannelInitializer; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; @@ -10,7 +9,6 @@ import io.netty.handler.logging.LoggingHandler; import lombok.extern.slf4j.Slf4j; import org.dromara.neutrinoproxy.server.base.proxy.ProxyConfig; import org.dromara.neutrinoproxy.server.proxy.core.BytesMetricsHandler; -import org.dromara.neutrinoproxy.server.proxy.core.ProxyTunnelServer; import org.dromara.neutrinoproxy.server.proxy.security.HttpVisitorSecurityChannelHandler; import org.dromara.neutrinoproxy.server.proxy.security.VisitorFlowLimiterChannelHandler; import org.noear.solon.annotation.Component; @@ -30,8 +28,8 @@ public class HttpProxy implements EventListener { private ProxyConfig proxyConfig; @Override public void onEvent(AppLoadEndEvent appLoadEndEvent) throws Throwable { - if (StrUtil.isBlank(proxyConfig.getServer().getTcp().getDomainName()) || null == proxyConfig.getServer().getTcp().getHttpProxyPort()) { - log.info("no config domain name,nonsupport http proxy."); + if (null == proxyConfig.getServer().getTcp().getHttpProxyPort()) { + log.info("no config http proxy port, nonsupport http proxy."); return; } this.start(); @@ -48,7 +46,7 @@ public class HttpProxy implements EventListener { ch.pipeline().addFirst(new LoggingHandler(HttpProxy.class)); } ch.pipeline().addFirst(new BytesMetricsHandler()); - ch.pipeline().addLast(new HttpVisitorSecurityChannelHandler(proxyConfig.getServer().getTcp().getDomainName())); + ch.pipeline().addLast(new HttpVisitorSecurityChannelHandler(false)); ch.pipeline().addLast("flowLimiter",new VisitorFlowLimiterChannelHandler()); ch.pipeline().addLast(new HttpVisitorChannelHandler()); } diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/proxy/enhance/HttpsProxy.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/proxy/enhance/HttpsProxy.java index aa5ff58e..478cb722 100644 --- a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/proxy/enhance/HttpsProxy.java +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/proxy/enhance/HttpsProxy.java @@ -1,19 +1,16 @@ package org.dromara.neutrinoproxy.server.proxy.enhance; -import cn.hutool.core.util.StrUtil; import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.*; +import io.netty.channel.ChannelInitializer; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.logging.LoggingHandler; -import io.netty.handler.ssl.SslHandler; +import io.netty.handler.ssl.SniHandler; +import io.netty.handler.ssl.SslContext; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.dromara.neutrinoproxy.core.util.FileUtil; import org.dromara.neutrinoproxy.server.base.proxy.ProxyConfig; import org.dromara.neutrinoproxy.server.proxy.core.BytesMetricsHandler; -import org.dromara.neutrinoproxy.server.proxy.core.ProxyTunnelServer; import org.dromara.neutrinoproxy.server.proxy.security.HttpVisitorSecurityChannelHandler; import org.dromara.neutrinoproxy.server.proxy.security.VisitorFlowLimiterChannelHandler; import org.noear.solon.annotation.Component; @@ -21,10 +18,6 @@ import org.noear.solon.annotation.Inject; import org.noear.solon.core.event.AppLoadEndEvent; import org.noear.solon.core.event.EventListener; -import javax.net.ssl.*; -import java.io.InputStream; -import java.security.KeyStore; - /** * HTTPS代理 * @author: aoshiguchen @@ -35,11 +28,13 @@ import java.security.KeyStore; public class HttpsProxy implements EventListener { @Inject private ProxyConfig proxyConfig; + @Inject + private SslContextManager sslContextManager; + @Override - public void onEvent(AppLoadEndEvent appLoadEndEvent) throws Throwable { - if (StrUtil.isBlank(proxyConfig.getServer().getTcp().getDomainName()) || null == proxyConfig.getServer().getTcp().getHttpsProxyPort() || - StringUtils.isEmpty(proxyConfig.getServer().getTcp().getJksPath()) || StringUtils.isEmpty(proxyConfig.getServer().getTcp().getKeyStorePassword())) { - log.info("no config domain name,nonsupport https proxy."); + public void onEvent(AppLoadEndEvent appLoadEndEvent) { + if (null == proxyConfig.getServer().getTcp().getHttpsProxyPort()) { + log.info("no config https proxy port, nonsupport http proxy."); return; } this.start(); @@ -55,9 +50,9 @@ public class HttpsProxy implements EventListener { if (null != proxyConfig.getServer().getTcp().getTransferLogEnable() && proxyConfig.getServer().getTcp().getTransferLogEnable()) { ch.pipeline().addFirst(new LoggingHandler(HttpsProxy.class)); } - ch.pipeline().addLast(createSslHandler()); + ch.pipeline().addLast(createSniHandler()); ch.pipeline().addFirst(new BytesMetricsHandler()); - ch.pipeline().addLast(new HttpVisitorSecurityChannelHandler(proxyConfig.getServer().getTcp().getDomainName())); + ch.pipeline().addLast(new HttpVisitorSecurityChannelHandler(true)); ch.pipeline().addLast("flowLimiter",new VisitorFlowLimiterChannelHandler()); ch.pipeline().addLast(new HttpVisitorChannelHandler()); } @@ -69,26 +64,19 @@ public class HttpsProxy implements EventListener { } } - private ChannelHandler createSslHandler() { + public SniHandler createSniHandler() { try { - InputStream jksInputStream = FileUtil.getInputStream(proxyConfig.getServer().getTcp().getJksPath()); - SSLContext serverContext = SSLContext.getInstance("TLS"); - final KeyStore ks = KeyStore.getInstance("JKS"); - - ks.load(jksInputStream, proxyConfig.getServer().getTcp().getKeyStorePassword().toCharArray()); - final KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - kmf.init(ks, proxyConfig.getServer().getTcp().getKeyStorePassword().toCharArray()); - TrustManager[] trustManagers = null; - - serverContext.init(kmf.getKeyManagers(), trustManagers, null); - - SSLEngine sslEngine = serverContext.createSSLEngine(); - sslEngine.setUseClientMode(false); - sslEngine.setNeedClientAuth(false); - - return new SslHandler(sslEngine); + return new SniHandler(domainName -> { + SslContext sslContext = sslContextManager.getSslContextByFullDomain(domainName); + if (sslContext != null) { + return sslContext; + } else { + log.info("No SSL context available for domain: {}", domainName); + return null; + } + }); } catch (Exception e) { - log.error("create SSL handler failed", e); + log.info("create SSL handler failed", e); e.printStackTrace(); } return null; diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/proxy/enhance/SslContextManager.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/proxy/enhance/SslContextManager.java new file mode 100644 index 00000000..f45ede72 --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/proxy/enhance/SslContextManager.java @@ -0,0 +1,126 @@ +package org.dromara.neutrinoproxy.server.proxy.enhance; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.solon.annotation.Db; +import org.dromara.neutrinoproxy.server.base.db.DBInitialize; +import org.dromara.neutrinoproxy.server.dal.DomainMapper; +import org.dromara.neutrinoproxy.server.dal.entity.DomainNameDO; +import org.dromara.neutrinoproxy.server.util.ProxyUtil; +import org.noear.solon.annotation.Component; +import org.noear.solon.annotation.Init; +import org.noear.solon.annotation.Inject; +import org.noear.solon.core.event.AppLoadEndEvent; +import org.noear.solon.core.event.EventListener; +import org.noear.solon.core.runtime.NativeDetector; + +import javax.net.ssl.KeyManagerFactory; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.KeyStore; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 域名到SSL上下文 + * @author: Mirac + * @date: 2024/8/25 + */ +@Slf4j +@Component +@Data +public class SslContextManager implements EventListener { + + @Db + private DomainMapper domainMapper; + + @Inject + private DBInitialize dbInitialize; + + // 初始化时加载所有域名的 SSL 上下文 + @Init + public void init() throws Throwable { + // aot 阶段,不初始化 + if (NativeDetector.isAotRuntime()) { + return; + } + try { + initializeSslContexts(); + } catch (Exception e) { + log.error("initialize SSL handler failed", e); + throw e; + } + } + + + @Override + public void onEvent(AppLoadEndEvent appLoadEndEvent) throws Throwable { + + } + + // 维护域名到 SslContext 的映射 + private final ConcurrentHashMap domainSslContexts = new ConcurrentHashMap<>(); + + // 初始化所有域名的 SSL 上下文 + private void initializeSslContexts() throws Exception { + List domainNameDOS = domainMapper.selectList(Wrappers.lambdaQuery() + .isNotNull(DomainNameDO::getDomain) + .isNotNull(DomainNameDO::getJks) + .isNotNull(DomainNameDO::getKeyStorePassword)); + + for (DomainNameDO domainNameDO : domainNameDOS) { + if (domainNameDO.getDomain() != null && domainNameDO.getKeyStorePassword() != null && domainNameDO.getJks() != null) { + String domain = domainNameDO.getDomain(); + byte[] jks = domainNameDO.getJks();// 假设数据库中存储了每个域名对应的 JKS 路径 + String keyStorePassword = domainNameDO.getKeyStorePassword(); + SslContext sslContext = loadSslContextFromJks(jks, keyStorePassword); + domainSslContexts.put(domain, sslContext); + } + } + } + + // 动态添加新的域名和证书 + public void addDomainAndCert(String domain, byte[] jks, String keyStorePassword) throws Exception { + SslContext sslContext = loadSslContextFromJks(jks, keyStorePassword); + domainSslContexts.put(domain, sslContext); + } + + // 使用 JKS 文件加载 SSL 上下文,禁用客户端认证并设置为服务器模式 + private SslContext loadSslContextFromJks(byte[] jks, String keyStorePassword) throws Exception { + InputStream jksInputStream = new ByteArrayInputStream(jks); + // 初始化 KeyStore + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(jksInputStream, keyStorePassword.toCharArray()); + + // 初始化 KeyManagerFactory + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, keyStorePassword.toCharArray()); + + // 创建 SslContext 并禁用客户端认证 + return SslContextBuilder.forServer(kmf) + .clientAuth(ClientAuth.NONE) // 禁用客户端认证 + .build(); + } + + public SslContext getSslContextByDomain(String domain) { + return domainSslContexts.get(domain); + } + + + public void removeSslContextByDomain(String domain) { + domainSslContexts.remove(domain); + } + + public SslContext getSslContextByFullDomain(String fullDomain) { + String domain = ProxyUtil.getDomainNameByFullDomain(fullDomain); + if (StrUtil.isBlank(domain)) { + return null; + } + return domainSslContexts.get(domain); + } +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/proxy/security/HttpVisitorSecurityChannelHandler.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/proxy/security/HttpVisitorSecurityChannelHandler.java index eccd2914..e817dfe3 100644 --- a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/proxy/security/HttpVisitorSecurityChannelHandler.java +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/proxy/security/HttpVisitorSecurityChannelHandler.java @@ -1,6 +1,5 @@ package org.dromara.neutrinoproxy.server.proxy.security; -import cn.hutool.core.util.StrUtil; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; @@ -9,6 +8,7 @@ import org.apache.commons.lang3.StringUtils; import org.dromara.neutrinoproxy.core.Constants; import org.dromara.neutrinoproxy.core.util.HttpUtil; import org.dromara.neutrinoproxy.core.util.IpUtil; +import org.dromara.neutrinoproxy.server.service.DomainService; import org.dromara.neutrinoproxy.server.service.PortMappingService; import org.dromara.neutrinoproxy.server.service.SecurityGroupService; import org.dromara.neutrinoproxy.server.util.ProxyUtil; @@ -22,48 +22,46 @@ import org.noear.solon.Solon; public class HttpVisitorSecurityChannelHandler extends ChannelInboundHandlerAdapter { private final SecurityGroupService securityGroupService = Solon.context().getBean(SecurityGroupService.class); private final PortMappingService portMappingService = Solon.context().getBean(PortMappingService.class); + private final DomainService domainService = Solon.context().getBean(DomainService.class); /** * 域名 */ - private String domainName; + private Boolean isHttps; - public HttpVisitorSecurityChannelHandler(String domainName) { - this.domainName = domainName; + public HttpVisitorSecurityChannelHandler(Boolean isHttps) { + this.isHttps = isHttps; } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - // 未配置域名则不支持通过域名访问 - if (StrUtil.isBlank(domainName)) { + ByteBuf buf = (ByteBuf) msg; + + // 获取Host请求头 + byte[] bytes = new byte[buf.readableBytes()]; + buf.readBytes(bytes); + String httpContent = new String(bytes); + String host = HttpUtil.getHostIgnorePort(httpContent); //test1.asgc.fun + + log.debug("HttpProxy host: {}", host); + if (StringUtils.isBlank(host)) { + ctx.channel().close(); + return; + } + // 判断域名是否被禁用或删除 + Integer domainNameId = ProxyUtil.getDomainNameIdByFullDomain(host); + if (domainNameId == null) { + ctx.channel().close(); + return; + } + // 域名映射强制https验证 + if (!isHttps && domainService.isOnlyHttps(domainNameId)) { ctx.channel().close(); return; } - ByteBuf buf = (ByteBuf) msg; - Integer serverPort = ctx.channel().attr(Constants.SERVER_PORT).get(); if (null == serverPort) { - // 获取Host请求头 - byte[] bytes = new byte[buf.readableBytes()]; - buf.readBytes(bytes); - String httpContent = new String(bytes); - String host = HttpUtil.getHostIgnorePort(httpContent); - - log.debug("HttpProxy host: {}", host); - if (StringUtils.isBlank(host)) { - ctx.channel().close(); - return; - } - - // 根据Host匹配端口映射 - if (!host.endsWith(domainName)) { - ctx.channel().close(); - return; - } - int index = host.lastIndexOf("." + domainName); - String subdomain = host.substring(0, index); - - // 根据域名拿到绑定的映射对应的cmdChannel - serverPort = ProxyUtil.getServerPortBySubdomain(subdomain); + // channel没有服务器端口信息,尝试根据完整域名拿到服务端端口 + serverPort = ProxyUtil.getServerPortByFullDomain(host); if (null == serverPort) { ctx.channel().close(); return; diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/service/DomainService.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/service/DomainService.java new file mode 100644 index 00000000..9caf383f --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/service/DomainService.java @@ -0,0 +1,301 @@ +package org.dromara.neutrinoproxy.server.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.text.StrPool; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.solon.plugins.pagination.Page; +import com.google.common.collect.Sets; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.solon.annotation.Db; +import org.dromara.neutrinoproxy.server.base.page.PageInfo; +import org.dromara.neutrinoproxy.server.base.page.PageQuery; +import org.dromara.neutrinoproxy.server.base.rest.SystemContextHolder; +import org.dromara.neutrinoproxy.server.constant.*; +import org.dromara.neutrinoproxy.server.controller.req.proxy.DomainCreateReq; +import org.dromara.neutrinoproxy.server.controller.req.proxy.DomainListReq; +import org.dromara.neutrinoproxy.server.controller.req.proxy.DomainUpdateEnableStatusReq; +import org.dromara.neutrinoproxy.server.controller.req.proxy.DomainUpdateReq; +import org.dromara.neutrinoproxy.server.controller.res.proxy.DomainListRes; +import org.dromara.neutrinoproxy.server.controller.res.proxy.DomainUpdateDefaultStatusRes; +import org.dromara.neutrinoproxy.server.controller.res.proxy.DomainUpdateEnableStatusRes; +import org.dromara.neutrinoproxy.server.dal.DomainMapper; +import org.dromara.neutrinoproxy.server.dal.DomainPortMappingMapper; +import org.dromara.neutrinoproxy.server.dal.PortMappingMapper; +import org.dromara.neutrinoproxy.server.dal.UserMapper; +import org.dromara.neutrinoproxy.server.dal.entity.DomainNameDO; +import org.dromara.neutrinoproxy.server.dal.entity.PortMappingDO; +import org.dromara.neutrinoproxy.server.dal.entity.UserDO; +import org.dromara.neutrinoproxy.server.proxy.enhance.SslContextManager; +import org.dromara.neutrinoproxy.server.service.bo.FullDomainNameBO; +import org.dromara.neutrinoproxy.server.util.ParamCheckUtil; +import org.dromara.neutrinoproxy.server.util.ProxyUtil; +import org.noear.solon.annotation.Component; +import org.noear.solon.annotation.Init; +import org.noear.solon.annotation.Inject; +import org.noear.solon.core.handle.UploadedFile; +import org.noear.solon.core.runtime.NativeDetector; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author Mirac + * @date 21/7/2024 + */ +@Slf4j +@Component +public class DomainService { + @Db + private DomainMapper domainMapper; + @Db + private UserMapper userMapper; + @Db + private PortMappingMapper portMappingMapper; + @Db + private DomainPortMappingMapper domainPortMappingMapper; + + @Inject + private SslContextManager sslContextManager; + + public PageInfo page(PageQuery pageQuery, DomainListReq req) { + Page page = domainMapper.selectPage(new Page<>(pageQuery.getCurrent(), pageQuery.getSize()), new LambdaQueryWrapper() + .eq(req.getEnable() != null, DomainNameDO::getEnable, req.getEnable()) + .eq(req.getUserId() != null, DomainNameDO::getUserId, req.getUserId()) + .orderByAsc(Arrays.asList(DomainNameDO::getUserId, DomainNameDO::getId)) + ); + List resList = page.getRecords().stream().map(DomainNameDO::toRes).collect(Collectors.toList()); + if (CollectionUtils.isEmpty(page.getRecords())) { + return PageInfo.of(resList, page); + } + Set userIds = resList.stream().map(DomainListRes::getUserId).collect(Collectors.toSet()); + List userDOList = userMapper.findByIds(userIds); + Map userMap = userDOList.stream().collect(Collectors.toMap(UserDO::getId, c -> c)); + for (DomainListRes item : resList) { + UserDO userDO = userMap.get(item.getUserId()); + if (null != userDO) { + item.setUserName(userDO.getName()); + } + } + return PageInfo.of(resList, page); + } + + public List all(DomainListReq req) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(req.getEnable() != null, DomainNameDO::getEnable, req.getEnable()); + List domainNameDOS = domainMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(domainNameDOS)) { + return null; + } + List resList = domainNameDOS.stream().map(DomainNameDO::toRes).toList(); + Set userIds = resList.stream().map(DomainListRes::getUserId).collect(Collectors.toSet()); + List userDOList = userMapper.findByIds(userIds); + Map userMap = userDOList.stream().collect(Collectors.toMap(UserDO::getId, c -> c)); + for (DomainListRes item : resList) { + UserDO userDO = userMap.get(item.getUserId()); + if (null != userDO) { + item.setUserName(userDO.getName()); + } + } + return resList; + } + + /** + * 创建域名 + * + * @param req + */ + public void create(DomainCreateReq req, UploadedFile jks) { + DomainNameDO domainNameCheck = domainMapper.checkRepeat(req.getDomain(), null); + ParamCheckUtil.checkMustNull(domainNameCheck, ExceptionConstant.DOMAIN_NAME_CANNOT_REPEAT); + DomainNameDO domainNameDO = new DomainNameDO(); + if (null != jks) { + try { + ParamCheckUtil.checkNotEmpty(req.getKeyStorePassword(), "keyStorePassword"); + + InputStream content = jks.getContent(); + byte[] byteArray = toByteArray(content); + domainNameDO.setJks(byteArray); + domainNameDO.setKeyStorePassword(req.getKeyStorePassword()); + //添加证书 + sslContextManager.addDomainAndCert(req.getDomain(), byteArray, req.getKeyStorePassword()); + } catch (Exception e) { + log.error("证书添加失败", e); + e.printStackTrace(); + } + } + Integer userId = SystemContextHolder.getUserId(); + Date now = new Date(); + domainNameDO.setDomain(req.getDomain()); + domainNameDO.setIsDefault(DefaultDomainStatusEnum.DISABLE.getStatus()); + domainNameDO.setForceHttps(req.getForceHttps() != null ? HttpsStatusEnum.of(req.getForceHttps()).getStatus() : HttpsStatusEnum.DISABLE_ONLY_HTTPS.getStatus()); + domainNameDO.setUserId(userId); + domainNameDO.setEnable(EnableStatusEnum.ENABLE.getStatus()); + domainNameDO.setCreateTime(now); + domainNameDO.setUpdateTime(now); + domainMapper.insert(domainNameDO); + //更新主域名到域名id映射 + ProxyUtil.setDomainToDomainNameId(domainNameDO.getDomain(), domainNameDO.getId()); + } + + /** + * 将 InputStream 转换为 byte[] + */ + private byte[] toByteArray(InputStream input) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int nRead; + byte[] data = new byte[1024]; + while ((nRead = input.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + buffer.flush(); + return buffer.toByteArray(); + } + + public void update(DomainUpdateReq req, UploadedFile jks) { + DomainNameDO domainNameCheck = domainMapper.checkRepeat(req.getDomain(), Sets.newHashSet(req.getId())); + ParamCheckUtil.checkMustNull(domainNameCheck, ExceptionConstant.DOMAIN_NAME_CANNOT_REPEAT); + + DomainNameDO oldDomainNameDO = domainMapper.selectOne(Wrappers.lambdaQuery().eq(DomainNameDO::getId, req.getId())); + List oldFullDomainNameBOS = domainMapper.selectFullDomainNameListByDomainNameIds(Sets.newHashSet(req.getId())); + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(DomainNameDO::getId, req.getId()); + if (null != jks) { + try { + ParamCheckUtil.checkNotEmpty(req.getKeyStorePassword(), "keyStorePassword"); + + InputStream content = jks.getContent(); + byte[] byteArray = toByteArray(content); + updateWrapper.set(DomainNameDO::getKeyStorePassword, req.getKeyStorePassword()); + updateWrapper.set(DomainNameDO::getJks, byteArray); + //添加证书并删除旧的证书 + sslContextManager.addDomainAndCert(req.getDomain(), byteArray, req.getKeyStorePassword()); + if (!req.getDomain().equals(oldDomainNameDO.getDomain())) { + sslContextManager.removeSslContextByDomain(oldDomainNameDO.getDomain()); + } + } catch (Exception e) { + log.error("证书添加失败", e); + e.printStackTrace(); + } + } + updateWrapper.set(DomainNameDO::getDomain, req.getDomain()); + updateWrapper.set(DomainNameDO::getUpdateTime, new Date()); + updateWrapper.set(req.getForceHttps() != null, DomainNameDO::getForceHttps, req.getForceHttps()); + domainMapper.update(updateWrapper); + + //更新域名相关映射 + if (!Objects.equals(oldDomainNameDO.getDomain(), req.getDomain())) { + //更新主域名到域名id映射 + ProxyUtil.removeDomainToDomainNameId(oldDomainNameDO.getDomain()); + ProxyUtil.setDomainToDomainNameId(req.getDomain(), req.getId()); + if (CollectionUtil.isEmpty(oldFullDomainNameBOS)) return; + //存在域名映射,更新完整域名到服务器端口映射 + for (FullDomainNameBO oldFullDomainNameBO : oldFullDomainNameBOS) { + String oldFullDomain = StrUtil.join(StrPool.DOT, oldFullDomainNameBO.getSubdomain(), oldFullDomainNameBO.getDomain()); + Integer serverPort = ProxyUtil.getServerPortByFullDomain(oldFullDomain); + ProxyUtil.removeFullDomainToServerPort(oldFullDomain); + ProxyUtil.setFullDomainToServerPort(StrUtil.join(StrPool.DOT, oldFullDomainNameBO.getSubdomain(), req.getDomain()), serverPort); + } + } + } + + public DomainUpdateEnableStatusRes updateEnableStatus(DomainUpdateEnableStatusReq req) { + if (Objects.equals(req.getEnable(), EnableStatusEnum.DISABLE.getStatus())) { + //如果设置状态为禁用,则将默认域名设置为非默认域名 + updateDefaultStatus(req.getId(), DefaultDomainStatusEnum.DISABLE.getStatus()); + } + domainMapper.updateEnableStatus(req.getId(), req.getEnable(), new Date()); + if (Objects.equals(req.getEnable(), EnableStatusEnum.ENABLE.getStatus())) { + ProxyUtil.setDomainToDomainNameId(req.getDomain(), req.getId()); + } else { + ProxyUtil.removeDomainToDomainNameId(req.getDomain()); + } + return new DomainUpdateEnableStatusRes(); + } + + public void delete(Integer domainNameId) { + //检查当前域名是否正在使用 + boolean checkUsed = domainPortMappingMapper.checkUsed(domainNameId); + ParamCheckUtil.checkExpression(!checkUsed, ExceptionConstant.DOMAIN_NAME_IS_USED); + DomainNameDO domainNameDO = domainMapper.selectOne(Wrappers.lambdaQuery() + .eq(DomainNameDO::getId, domainNameId) + .select(DomainNameDO::getDomain, DomainNameDO::getEnable)); + if (Objects.equals(domainNameDO.getEnable(), EnableStatusEnum.ENABLE.getStatus())) { + ProxyUtil.removeDomainToDomainNameId(domainNameDO.getDomain()); + } + domainMapper.deleteById(domainNameId); + //删除证书 + sslContextManager.removeSslContextByDomain(domainNameDO.getDomain()); + } + + public DomainUpdateDefaultStatusRes updateDefaultStatus(Integer id, Integer isDefault) { + if (Objects.equals(isDefault, DefaultDomainStatusEnum.ENABLE.getStatus())) { + //如果设置状态为默认域名,则将之前的默认域名设置为非默认域名 + DomainNameDO oldDomainNameDO = domainMapper.selectOne(Wrappers.lambdaQuery() + .select(DomainNameDO::getId) + .eq(DomainNameDO::getIsDefault, DefaultDomainStatusEnum.ENABLE.getStatus())); + if (oldDomainNameDO != null) { + oldDomainNameDO.setIsDefault(DefaultDomainStatusEnum.DISABLE.getStatus()); + domainMapper.updateById(oldDomainNameDO); + } + } + + DomainNameDO domainNameDO = new DomainNameDO(); + domainNameDO.setId(id); + domainNameDO.setIsDefault(isDefault); + domainMapper.updateById(domainNameDO); + + return new DomainUpdateDefaultStatusRes(); + } + + @Init + public void init() { + // aot 阶段,不初始化 + if (NativeDetector.isAotRuntime()) { + return; + } + List fullDomainNameBOS = domainMapper.selectFullDomainNameList(); + if (CollectionUtil.isEmpty(fullDomainNameBOS)) { + return; + } + Set portMappingIds = fullDomainNameBOS.stream().map(FullDomainNameBO::getPortMappingId).collect(Collectors.toSet()); + List portMappingDOS = portMappingMapper.selectBatchIds(portMappingIds); + if (CollectionUtil.isEmpty(portMappingDOS)) { + return; + } + Map portMappingDOMap = portMappingDOS.stream() + .filter(item -> NetworkProtocolEnum.HTTP.getDesc().equals(item.getProtocal())) + .collect(Collectors.toMap(PortMappingDO::getId, Function.identity())); + fullDomainNameBOS.forEach(item -> { + PortMappingDO portMappingDO = portMappingDOMap.get(item.getPortMappingId()); + if (null == portMappingDO) { + return; + } + String fullDomain = StrUtil.join(StrPool.DOT, item.getSubdomain(), item.getDomain()); + //更新完整域名到端口映射 + ProxyUtil.setFullDomainToServerPort(fullDomain, portMappingDO.getServerPort()); + }); + + //更新主域名到主域名id缓存,未禁用 + List domainNameDOS = domainMapper.selectList(Wrappers.lambdaQuery() + .eq(DomainNameDO::getEnable, EnableStatusEnum.ENABLE.getStatus()) + .select(DomainNameDO::getDomain, DomainNameDO::getId)); + for (DomainNameDO domainNameDO : domainNameDOS) { + ProxyUtil.setDomainToDomainNameId(domainNameDO.getDomain(), domainNameDO.getId()); + } + } + + public boolean isOnlyHttps(Integer domainNameId) { + ParamCheckUtil.checkExpression((domainNameId != null && domainNameId > 0), ExceptionConstant.DOMAIN_NAME_NOT_EXIST); + DomainNameDO domainNameDO = domainMapper.selectOne(Wrappers.lambdaQuery() + .eq(DomainNameDO::getId, domainNameId)); + return domainNameDO != null && Objects.equals(domainNameDO.getForceHttps(), HttpsStatusEnum.ONLY_HTTPS.getStatus()); + } +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/service/PortMappingService.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/service/PortMappingService.java index 5ce15105..0a7a427b 100644 --- a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/service/PortMappingService.java +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/service/PortMappingService.java @@ -2,8 +2,8 @@ package org.dromara.neutrinoproxy.server.service; import cn.hutool.cache.Cache; import cn.hutool.cache.CacheUtil; -import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.text.StrPool; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; @@ -30,16 +30,10 @@ import org.dromara.neutrinoproxy.server.controller.res.proxy.PortMappingCreateRe import org.dromara.neutrinoproxy.server.controller.res.proxy.PortMappingDetailRes; import org.dromara.neutrinoproxy.server.controller.res.proxy.PortMappingListRes; import org.dromara.neutrinoproxy.server.controller.res.proxy.PortMappingUpdateEnableStatusRes; -import org.dromara.neutrinoproxy.server.controller.res.proxy.PortMappingUpdateRes; -import org.dromara.neutrinoproxy.server.dal.LicenseMapper; -import org.dromara.neutrinoproxy.server.dal.PortMappingMapper; -import org.dromara.neutrinoproxy.server.dal.PortPoolMapper; -import org.dromara.neutrinoproxy.server.dal.UserMapper; -import org.dromara.neutrinoproxy.server.dal.entity.LicenseDO; -import org.dromara.neutrinoproxy.server.dal.entity.PortMappingDO; -import org.dromara.neutrinoproxy.server.dal.entity.PortPoolDO; -import org.dromara.neutrinoproxy.server.dal.entity.UserDO; +import org.dromara.neutrinoproxy.server.dal.*; +import org.dromara.neutrinoproxy.server.dal.entity.*; import org.dromara.neutrinoproxy.server.service.bo.FlowLimitBO; +import org.dromara.neutrinoproxy.server.service.bo.FullDomainNameBO; import org.dromara.neutrinoproxy.server.util.ParamCheckUtil; import org.dromara.neutrinoproxy.server.util.ProxyUtil; import org.dromara.neutrinoproxy.server.util.StringUtil; @@ -68,6 +62,10 @@ public class PortMappingService implements LifecycleBean { private UserMapper userMapper; @Db private PortPoolMapper portPoolMapper; + @Db + private DomainMapper domainMapper; + @Db + private DomainPortMappingMapper domainPortMappingMapper; @Inject private VisitorChannelService visitorChannelService; @@ -82,7 +80,6 @@ public class PortMappingService implements LifecycleBean { /** 端口到安全组Id的映射 */ private final Map mappingPortToSecurityGroupMap = new ConcurrentHashMap<>(); - // 服务端端口到端口映射id的映射 private final Cache serverPortToPortMappingIdCache = CacheUtil.newLRUCache(500, 1000 * 60 * 10); // 端口映射id到licenseId private final Cache idToLicenseIdCache = CacheUtil.newLRUCache(500, 1000 * 60 * 10); @@ -118,6 +115,11 @@ public class PortMappingService implements LifecycleBean { Map licenseMap = licenseList.stream().collect(Collectors.toMap(LicenseDO::getId, Function.identity())); Map userMap = userList.stream().collect(Collectors.toMap(UserDO::getId, Function.identity())); + //域名相关 + Set portMappingIds = respList.stream().map(PortMappingListRes::getId).collect(Collectors.toSet()); + List fullDomainNameBOS = domainMapper.selectFullDomainNameListByPortMappingIds(portMappingIds); + Map> fullDomainNameBOMap = fullDomainNameBOS.stream().collect(Collectors.groupingBy(FullDomainNameBO::getPortMappingId)); + respList.forEach(item -> { LicenseDO license = licenseMap.get(item.getLicenseId()); if (null == license) { @@ -130,9 +132,8 @@ public class PortMappingService implements LifecycleBean { return; } item.setUserName(user.getName()); - if (StrUtil.isNotBlank(proxyConfig.getServer().getTcp().getDomainName()) && StrUtil.isNotBlank(item.getSubdomain())) { - item.setDomain(item.getSubdomain() + "." + proxyConfig.getServer().getTcp().getDomainName()); - } + //域名映射 + item.setFullDomainMappings(fullDomainNameBOMap.get(item.getId())); if (NetworkProtocolEnum.HTTP.getDesc().equals(item.getProtocal())) { item.setProtocal("HTTP(S)"); } @@ -154,13 +155,25 @@ public class PortMappingService implements LifecycleBean { PortPoolDO portPoolDO = portPoolMapper.findByPort(req.getServerPort()); ParamCheckUtil.checkNotNull(portPoolDO, ExceptionConstant.PORT_NOT_EXIST); ParamCheckUtil.checkExpression(null == portMappingMapper.findByPort(req.getServerPort(), null), ExceptionConstant.PORT_CANNOT_REPEAT_MAPPING, req.getServerPort()); - ParamCheckUtil.checkExpression(!portMappingMapper.checkRepeatBySubdomain(req.getSubdomain(), null), ExceptionConstant.PORT_MAPPING_SUBDONAME_CONNOT_REPEAT); + //验证域名映射相关参数条件 + if (NetworkProtocolEnum.isHttp(req.getProtocal()) && CollectionUtil.isNotEmpty(req.getDomainMappings())) { + Set domainIds = req.getDomainMappings().stream().map(item -> item.getDomainId()).collect(Collectors.toSet()); + List domainNameDOS = domainMapper.selectBatchIds(domainIds); + Map domainNameDOMap = domainNameDOS.stream().collect(Collectors.toMap(DomainNameDO::getId, Function.identity())); + req.getDomainMappings().forEach(item -> { + DomainNameDO domainNameDO = domainNameDOMap.get(item.getDomainId()); + ParamCheckUtil.checkNotNull(domainNameDO, ExceptionConstant.DOMAIN_NAME_NOT_EXIST); + //检查当前域名是否被禁用 + //ParamCheckUtil.checkExpression(Objects.equals(EnableStatusEnum.ENABLE.getStatus(), domainNameDO.getEnable()), ExceptionConstant.DOMAIN_NAME_IS_DISABLE, domainNameDO.getDomain()); + //检查子域名是否重复 + ParamCheckUtil.checkExpression(!domainPortMappingMapper.checkRepeatBySubdomain(item.getSubdomain(), item.getDomainId(), null), ExceptionConstant.SUDOMAIN_NAME_CANNOT_REPEAT); + }); + } Date now = new Date(); PortMappingDO portMappingDO = new 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()); @@ -176,9 +189,24 @@ public class PortMappingService implements LifecycleBean { portMappingMapper.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()); + + // 更新域名映射,添加新的域名映射 + if (NetworkProtocolEnum.isHttp(portMappingDO.getProtocal()) && CollectionUtil.isNotEmpty(req.getDomainMappings())) { + Set domainIds = req.getDomainMappings().stream().map(item -> item.getDomainId()).collect(Collectors.toSet()); + List domainNameDOS = domainMapper.selectBatchIds(domainIds); + Map domainNameDOMap = domainNameDOS.stream().collect(Collectors.toMap(DomainNameDO::getId, Function.identity())); + + req.getDomainMappings().forEach(item -> { + DomainNameDO domainNameDO = domainNameDOMap.get(item.getDomainId()); + //创建域名映射 + DomainPortMappingDO domainPortMappingDO = new DomainPortMappingDO(); + domainPortMappingDO.setDomainNameId(item.getDomainId()); + domainPortMappingDO.setSubdomain(item.getSubdomain()); + domainPortMappingDO.setPortMappingId(portMappingDO.getId()); + domainPortMappingMapper.insert(domainPortMappingDO); + //设置完整域名到服务端端口的映射 + ProxyUtil.setFullDomainToServerPort(StrUtil.join(StrPool.DOT, item.getSubdomain(), domainNameDO.getDomain()), portMappingDO.getServerPort()); + }); } updateMappingPortToSecurityGroupMap(portMappingDO.getServerPort(), req.getSecurityGroupId()); @@ -203,17 +231,29 @@ public class PortMappingService implements LifecycleBean { PortPoolDO portPoolDO = portPoolMapper.findByPort(req.getServerPort()); ParamCheckUtil.checkNotNull(portPoolDO, ExceptionConstant.PORT_NOT_EXIST); ParamCheckUtil.checkExpression(null == portMappingMapper.findByPort(req.getServerPort(), Sets.newHashSet(req.getId())), ExceptionConstant.PORT_CANNOT_REPEAT_MAPPING, req.getServerPort()); - ParamCheckUtil.checkExpression(!portMappingMapper.checkRepeatBySubdomain(req.getSubdomain(), Sets.newHashSet(req.getId())), ExceptionConstant.PORT_MAPPING_SUBDONAME_CONNOT_REPEAT); - // 查询原端口映射 PortMappingDO oldPortMappingDO = portMappingMapper.findById(req.getId()); ParamCheckUtil.checkNotNull(oldPortMappingDO, ExceptionConstant.PORT_MAPPING_NOT_EXIST); + //验证域名映射相关参数条件 + if (NetworkProtocolEnum.isHttp(req.getProtocal()) && CollectionUtil.isNotEmpty(req.getDomainMappings())) { + Set domainIds = req.getDomainMappings().stream().map(item -> item.getDomainId()).collect(Collectors.toSet()); + List domainNameDOS = domainMapper.selectBatchIds(domainIds); + Map domainNameDOMap = domainNameDOS.stream().collect(Collectors.toMap(DomainNameDO::getId, Function.identity())); + + req.getDomainMappings().forEach(item -> { + DomainNameDO domainNameDO = domainNameDOMap.get(item.getDomainId()); + ParamCheckUtil.checkNotNull(domainNameDO, ExceptionConstant.DOMAIN_NAME_NOT_EXIST); + //检查子域名是否重复 + boolean b = !domainPortMappingMapper.checkRepeatBySubdomain(item.getSubdomain(), item.getDomainId(), item.getId()); + ParamCheckUtil.checkExpression(b, + ExceptionConstant.SUDOMAIN_NAME_CANNOT_REPEAT); + }); + } // 更新端口映射 portMappingMapper.update(null, new LambdaUpdateWrapper() .eq(PortMappingDO::getId, req.getId()) .set(PortMappingDO::getProtocal, req.getProtocal()) - .set(PortMappingDO::getSubdomain, req.getSubdomain()) .set(PortMappingDO::getServerPort, req.getServerPort()) .set(PortMappingDO::getClientIp, req.getClientIp()) .set(PortMappingDO::getClientPort, req.getClientPort()) @@ -229,13 +269,34 @@ public class PortMappingService implements LifecycleBean { // 更新VisitorChannel PortMappingDO portMappingDO = portMappingMapper.findById(req.getId()); visitorChannelService.updateVisitorChannelByPortMapping(oldPortMappingDO, portMappingDO); - // 删除老的域名映射 - if (NetworkProtocolEnum.isHttp(oldPortMappingDO.getProtocal()) && StrUtil.isNotBlank(oldPortMappingDO.getSubdomain())) { - ProxyUtil.removeSubdomainToServerPort(oldPortMappingDO.getSubdomain()); - } + // 更新域名映射 - if (NetworkProtocolEnum.isHttp(portMappingDO.getProtocal()) && StrUtil.isNotBlank(proxyConfig.getServer().getTcp().getDomainName()) && StrUtil.isNotBlank(portMappingDO.getSubdomain())) { - ProxyUtil.setSubdomainToServerPort(portMappingDO.getSubdomain(), portMappingDO.getServerPort()); + // 删除老的域名映射 + if (NetworkProtocolEnum.isHttp(oldPortMappingDO.getProtocal())) { + //删除完整域名到服务端端口的映射 + ProxyUtil.removeFullDomainToServerPortByServerPort(oldPortMappingDO.getServerPort()); + //删除旧的域名映射 + LambdaQueryWrapper lambdaQueryWrapper = Wrappers.lambdaQuery() + .eq(DomainPortMappingDO::getPortMappingId, oldPortMappingDO.getId()); + domainPortMappingMapper.delete(lambdaQueryWrapper); + } + // 添加新的域名映射 + if (NetworkProtocolEnum.isHttp(portMappingDO.getProtocal()) && CollectionUtil.isNotEmpty(req.getDomainMappings())) { + Set domainIds = req.getDomainMappings().stream().map(item -> item.getDomainId()).collect(Collectors.toSet()); + List domainNameDOS = domainMapper.selectBatchIds(domainIds); + Map domainNameDOMap = domainNameDOS.stream().collect(Collectors.toMap(DomainNameDO::getId, Function.identity())); + + req.getDomainMappings().forEach(item -> { + DomainNameDO domainNameDO = domainNameDOMap.get(item.getDomainId()); + //创建域名映射 + DomainPortMappingDO domainPortMappingDO = new DomainPortMappingDO(); + domainPortMappingDO.setDomainNameId(item.getDomainId()); + domainPortMappingDO.setSubdomain(item.getSubdomain()); + domainPortMappingDO.setPortMappingId(portMappingDO.getId()); + domainPortMappingMapper.insert(domainPortMappingDO); + //设置完整域名到服务端端口的映射 + ProxyUtil.setFullDomainToServerPort(StrUtil.join(StrPool.DOT, item.getSubdomain(), domainNameDO.getDomain()), portMappingDO.getServerPort()); + }); } updateMappingPortToSecurityGroupMap(portMappingDO.getServerPort(), req.getSecurityGroupId()); @@ -316,9 +377,15 @@ public class PortMappingService implements LifecycleBean { // 更新VisitorChannel visitorChannelService.removeVisitorChannelByPortMapping(portMappingDO); - // 更新域名映射 - if (NetworkProtocolEnum.isHttp(portMappingDO.getProtocal()) && StrUtil.isNotBlank(portMappingDO.getSubdomain())) { - ProxyUtil.removeSubdomainToServerPort(portMappingDO.getSubdomain()); + + // 删除域名映射 + if (NetworkProtocolEnum.isHttp(portMappingDO.getProtocal())) { + //删除完整域名到服务端端口的映射 + ProxyUtil.removeFullDomainToServerPortByServerPort(portMappingDO.getServerPort()); + //删除旧的域名映射 + LambdaQueryWrapper lambdaQueryWrapper = Wrappers.lambdaQuery() + .eq(DomainPortMappingDO::getPortMappingId, portMappingDO.getId()); + domainPortMappingMapper.delete(lambdaQueryWrapper); } updateMappingPortToSecurityGroupMap(portMappingDO.getServerPort(), null); @@ -391,24 +458,6 @@ public class PortMappingService implements LifecycleBean { // 刷新流量限制缓存 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.getSubdomain() != null) - .collect(Collectors.toList()); - if (CollectionUtil.isEmpty(portMappingDOList)) { - return; - } - portMappingDOList.forEach(item -> { - if (StrUtil.isBlank(item.getSubdomain())) { - return; - } - ProxyUtil.setSubdomainToServerPort(item.getSubdomain(), item.getServerPort()); - - }); } /** diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/service/bo/FullDomainNameBO.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/service/bo/FullDomainNameBO.java new file mode 100644 index 00000000..892a453e --- /dev/null +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/service/bo/FullDomainNameBO.java @@ -0,0 +1,40 @@ +package org.dromara.neutrinoproxy.server.service.bo; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dromara.neutrinoproxy.server.constant.HttpsStatusEnum; + +/** + * 完整域名 + * @author: mirac + * @date: 2024/8/8 + */ +@Data +@Accessors(chain = true) +public class FullDomainNameBO { + /** + * 域名端口映射id + */ + private Integer id; + /** + * 端口映射id + */ + private Integer portMappingId; + /** + * 子域名 + */ + private String subdomain; + /** + * 域名id + */ + private Integer domainNameId; + /** + * 域名 + */ + private String domain; + /** + * 强制使用HTTPS(1、是 2、否) + * {@link HttpsStatusEnum} + */ + private Integer forceHttps; +} diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/util/ProxyUtil.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/util/ProxyUtil.java index 14ecd297..b163dca2 100644 --- a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/util/ProxyUtil.java +++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/util/ProxyUtil.java @@ -4,7 +4,6 @@ import cn.hutool.core.collection.CollectionUtil; import com.google.common.collect.Sets; import org.apache.commons.lang3.StringUtils; import org.dromara.neutrinoproxy.core.ChannelAttribute; -import org.dromara.neutrinoproxy.core.Constants; import org.dromara.neutrinoproxy.server.constant.NetworkProtocolEnum; import org.dromara.neutrinoproxy.server.proxy.domain.CmdChannelAttachInfo; import org.dromara.neutrinoproxy.server.proxy.domain.ProxyAttachment; @@ -19,6 +18,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Collectors; /** * @@ -32,7 +32,7 @@ public class ProxyUtil { */ private static final Map> licenseToServerPortMap = new HashMap<>(); /** - * 代理信息映射 + * 代理信息映射 e.g.: 9104 -> 127.0.0.1:8848 */ private static final Map proxyInfoMap = new ConcurrentHashMap<>(); /** @@ -61,9 +61,13 @@ public class ProxyUtil { */ private static Map proxyConnectAttachmentMap = new HashMap<>(); /** - * 子域名 - 服务端端口映射 + * 完整域名 - 服务端端口映射 */ - private static Map subdomainToServerPort = new HashMap<>(); + private static Map fullDomainToServerPortMap = new ConcurrentHashMap<>(); + /** + * 主域名 - 域名id映射 + */ + private static Map domainToDomainNameIdMap = new ConcurrentHashMap<>(); /** * licenseId - 客户端Id映射 */ @@ -339,32 +343,85 @@ public class ProxyUtil { } /** - * 设置子域名到服务端端口的映射 - * @param subdomain + * 设置完整域名到服务端端口的映射 + * @param fullDomain * @param serverPort */ - public static void setSubdomainToServerPort(String subdomain, Integer serverPort) { - subdomainToServerPort.put(subdomain, serverPort); + public static void setFullDomainToServerPort(String fullDomain, Integer serverPort) { + fullDomainToServerPortMap.put(fullDomain, serverPort); } /** - * 删除子域名到服务端端口的映射 - * @param subdomain + * 删除完整域名到服务端端口的映射 + * @param fullDomain */ - public static void removeSubdomainToServerPort(String subdomain) { - subdomainToServerPort.remove(subdomain); + public static void removeFullDomainToServerPort(String fullDomain) { + fullDomainToServerPortMap.remove(fullDomain); } + /** + * 通过服务器端口删除完整域名到服务端端口的映射 + * @param serverPort + */ + public static void removeFullDomainToServerPortByServerPort(Integer serverPort) { + fullDomainToServerPortMap.entrySet().removeIf(entry -> Objects.equals(entry.getValue(), serverPort)); + } + /** - * 根据子域名获取外网端口 - * @param subdomain + * 根据完整域名获取外网端口 + * @param fullDomain * @return */ - public static Integer getServerPortBySubdomain(String subdomain) { - return subdomainToServerPort.get(subdomain); + public static Integer getServerPortByFullDomain(String fullDomain) { + return fullDomainToServerPortMap.get(fullDomain); } - /** + /** + * 添加域名到域名id的映射 + */ + public static void setDomainToDomainNameId(String domain, Integer domainNameId) { + domainToDomainNameIdMap.put(domain, domainNameId); + } + + /** + * 删除域名到域名id的映射 + */ + public static void removeDomainToDomainNameId(String domain) { + domainToDomainNameIdMap.remove(domain); + } + + /** + * 通过主域名获取域名id + */ + public static Integer getDomainNameIdByDomain(String domain) { + return domainToDomainNameIdMap.get(domain); + } + + /** + * 通过完整域名获取域名id + */ + public static Integer getDomainNameIdByFullDomain(String fullDomain) { + List domains = domainToDomainNameIdMap.keySet().stream().filter(item -> fullDomain.endsWith(item)).collect(Collectors.toList()); + // 不存在 或者 有多条记录,返回null + if (CollectionUtil.isEmpty(domains) || domains.size() > 1) { + return null; + } + return getDomainNameIdByDomain(domains.get(0)); + } + + /** + * 通过完整域名获取域名 + */ + public static String getDomainNameByFullDomain(String fullDomain) { + List domains = domainToDomainNameIdMap.keySet().stream().filter(item -> fullDomain.endsWith(item)).collect(Collectors.toList()); + // 不存在 或者 有多条记录,返回null + if (CollectionUtil.isEmpty(domains) || domains.size() > 1) { + return null; + } + return domains.getFirst(); + } + + /** * 关闭http响应channel * @param channel * @return diff --git a/neutrino-proxy-server/src/main/resources/app-copy.yml b/neutrino-proxy-server/src/main/resources/app-copy.yml index e7360e41..e0fee879 100644 --- a/neutrino-proxy-server/src/main/resources/app-copy.yml +++ b/neutrino-proxy-server/src/main/resources/app-copy.yml @@ -34,15 +34,10 @@ neutrino: # 线程池相关配置,用于技术调优,可忽略 boss-thread-count: 5 work-thread-count: 20 - # http代理端口,默认80 + # http代理端口,默认80(不配置不支持) http-proxy-port: 80 - # https代理端口,默认443 (需要配置域名、证书) + # https代理端口,默认443 (不配置不支持,同时需要配置域名、证书) https-proxy-port: 443 - # 如果不配置,则不支持域名映射 - domain-name: - # https证书配置 - key-store-password: - jks-path: # 是否开启代理服务报文日志(日志级别为debug时开启才有效) transfer-log-enable: false udp: diff --git a/neutrino-proxy-server/src/main/resources/app.yml b/neutrino-proxy-server/src/main/resources/app.yml index c62aa9d0..e5d6be64 100644 --- a/neutrino-proxy-server/src/main/resources/app.yml +++ b/neutrino-proxy-server/src/main/resources/app.yml @@ -54,15 +54,10 @@ neutrino: # 线程池相关配置,用于技术调优,可忽略 boss-thread-count: 5 work-thread-count: 20 - # http代理端口,默认80 + # http代理端口,默认80(不配置不支持) http-proxy-port: ${HTTP_PROXY_PORT:80} - # https代理端口,默认443 (需要配置域名、证书) + # https代理端口,默认443 (不配置不支持,同时需要配置域名、证书) https-proxy-port: ${HTTPS_PROXY_PORT:443} - # 如果不配置,则不支持域名映射 - domain-name: ${DOMAIN_NAME:} - # https证书配置 - key-store-password: ${HTTPS_STORE_PASS:} - jks-path: ${HTTPS_JKS_PATH:} # 是否开启代理服务报文日志(日志级别为debug时开启才有效) transfer-log-enable: ${SERVER_LOG:false} udp: diff --git a/neutrino-proxy-server/src/main/resources/mapper/DomainMapper.xml b/neutrino-proxy-server/src/main/resources/mapper/DomainMapper.xml new file mode 100644 index 00000000..ce750ddd --- /dev/null +++ b/neutrino-proxy-server/src/main/resources/mapper/DomainMapper.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/neutrino-proxy-server/src/main/resources/sql/h2/init-structure.sql b/neutrino-proxy-server/src/main/resources/sql/h2/init-structure.sql index bfeaeda7..5ef08fa7 100644 --- a/neutrino-proxy-server/src/main/resources/sql/h2/init-structure.sql +++ b/neutrino-proxy-server/src/main/resources/sql/h2/init-structure.sql @@ -97,7 +97,6 @@ CREATE TABLE IF NOT EXISTS `port_mapping` ( `id` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, `license_id` INTEGER(20) NOT NULL, `protocal` VARCHAR(10) NOT NULL DEFAULT 'TCP', - `subdomain` VARCHAR(50) DEFAULT NULL, `server_port` INTEGER NOT NULL, `client_ip` VARCHAR(20) NOT NULL, `client_port` INTEGER NOT NULL, @@ -114,6 +113,32 @@ CREATE TABLE IF NOT EXISTS `port_mapping` ( ); CREATE UNIQUE INDEX IF NOT EXISTS I_port_mapping_server_port ON port_mapping (server_port ASC); +#域名表 +CREATE TABLE IF NOT EXISTS `domain_name` ( + `id` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, + `domain` VARCHAR(50) NOT NULL, + `user_id` INTEGER NOT NULL, + `jks` BLOB DEFAULT NULL, + `key_store_password` varchar(255) DEFAULT NULL, + `is_default` INTEGER(2) NOT NULL, + `force_https` INTEGER(2) NOT NULL, + `enable` INTEGER(2) NOT NULL, + `create_time` TIMESTAMP NOT NULL, + `update_time` TIMESTAMP NOT NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS I_domain_name_domain ON domain_name (`domain` ASC); + +#域名映射中间表 +CREATE TABLE IF NOT EXISTS `domain_port_mapping` ( + `id` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, + `subdomain` VARCHAR(50) NOT NULL, + `domain_name_id` INTEGER NOT NULL, + `port_mapping_id` INTEGER NOT NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS I_unique_domain_port_mapping ON domain_port_mapping (`domain_name_id` ASC, `subdomain` ASC); +CREATE INDEX IF NOT EXISTS I_domain_port_mapping_domain_name_id ON domain_port_mapping (`domain_name_id` ASC); +CREATE INDEX IF NOT EXISTS I_domain_port_mapping_port_mapping_id ON domain_port_mapping (`port_mapping_id` ASC); + #############################日志管理相关表############################# #用户登录记录表 CREATE TABLE IF NOT EXISTS `user_login_record` ( diff --git a/neutrino-proxy-server/src/main/resources/sql/h2/port_mapping.data.sql b/neutrino-proxy-server/src/main/resources/sql/h2/port_mapping.data.sql index 403143a1..c0d1e98e 100644 --- a/neutrino-proxy-server/src/main/resources/sql/h2/port_mapping.data.sql +++ b/neutrino-proxy-server/src/main/resources/sql/h2/port_mapping.data.sql @@ -1,7 +1,7 @@ #port_mapping -INSERT INTO port_mapping(`id`, `license_id`, `server_port`, `protocal`, `subdomain`,`client_ip`, `client_port`, `is_online`, `enable`,`description`,`create_time`, `update_time`) VALUES -(1, 1, 9101, 'HTTP', 'test1', '127.0.0.1', 8080, 2, 1,'test1', now(), now()); -INSERT INTO port_mapping(`id`, `license_id`, `server_port`, `protocal`, `subdomain`, `client_ip`, `client_port`, `is_online`, `enable`,`description`, `create_time`, `update_time`) VALUES -(2, 1, 9102, 'TCP', '', '127.0.0.1', 3306, 2, 1,'test2', now(), now()); -INSERT INTO port_mapping(`id`, `license_id`, `server_port`, `protocal`, `subdomain`, `client_ip`, `client_port`, `is_online`, `enable`,`description`, `create_time`, `update_time`) VALUES -(3, 1, 9103, 'HTTP', 'test2', '127.0.0.1', 8081, 2, 1,'test3', now(), now()); \ No newline at end of file +INSERT INTO port_mapping(`id`, `license_id`, `server_port`, `protocal`,`client_ip`, `client_port`, `is_online`, `enable`,`description`,`create_time`, `update_time`) VALUES +(1, 1, 9101, 'HTTP', '127.0.0.1', 8080, 2, 1,'test1', now(), now()); +INSERT INTO port_mapping(`id`, `license_id`, `server_port`, `protocal`, `client_ip`, `client_port`, `is_online`, `enable`,`description`, `create_time`, `update_time`) VALUES +(2, 1, 9102, 'TCP', '127.0.0.1', 3306, 2, 1,'test2', now(), now()); +INSERT INTO port_mapping(`id`, `license_id`, `server_port`, `protocal`, `client_ip`, `client_port`, `is_online`, `enable`,`description`, `create_time`, `update_time`) VALUES +(3, 1, 9103, 'HTTP', '127.0.0.1', 8081, 2, 1,'test3', now(), now()); \ No newline at end of file diff --git a/neutrino-proxy-server/src/main/resources/sql/h2/update/UPDATE-20240808.SQL b/neutrino-proxy-server/src/main/resources/sql/h2/update/UPDATE-20240808.SQL new file mode 100644 index 00000000..50c78aaa --- /dev/null +++ b/neutrino-proxy-server/src/main/resources/sql/h2/update/UPDATE-20240808.SQL @@ -0,0 +1,28 @@ +#域名表 +CREATE TABLE IF NOT EXISTS `domain_name` ( + `id` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, + `domain` VARCHAR(50) NOT NULL, + `user_id` INTEGER NOT NULL, + `jks` BLOB DEFAULT NULL, + `key_store_password` varchar(255) DEFAULT NULL, + `is_default` INTEGER(2) NOT NULL, + `force_https` INTEGER(2) NOT NULL, + `enable` INTEGER(2) NOT NULL, + `create_time` TIMESTAMP NOT NULL, + `update_time` TIMESTAMP NOT NULL + ); +CREATE UNIQUE INDEX IF NOT EXISTS I_domain_name_domain ON domain_name (`domain` ASC); + +#域名映射中间表 +CREATE TABLE IF NOT EXISTS `domain_port_mapping` ( + `id` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, + `subdomain` VARCHAR(50) NOT NULL, + `domain_name_id` INTEGER NOT NULL, + `port_mapping_id` INTEGER NOT NULL + ); +CREATE UNIQUE INDEX IF NOT EXISTS I_unique_domain_port_mapping ON domain_port_mapping (`domain_name_id` ASC, `subdomain` ASC); +CREATE INDEX IF NOT EXISTS I_domain_port_mapping_domain_name_id ON domain_port_mapping (`domain_name_id` ASC); +CREATE INDEX IF NOT EXISTS I_domain_port_mapping_port_mapping_id ON domain_port_mapping (`port_mapping_id` ASC); + +# port_mapping表删除字段 +ALTER TABLE `port_mapping` DROP COLUMN `subdomain`; diff --git a/neutrino-proxy-server/src/main/resources/sql/mariadb/init-structure.sql b/neutrino-proxy-server/src/main/resources/sql/mariadb/init-structure.sql index 76300f97..183de165 100644 --- a/neutrino-proxy-server/src/main/resources/sql/mariadb/init-structure.sql +++ b/neutrino-proxy-server/src/main/resources/sql/mariadb/init-structure.sql @@ -102,7 +102,6 @@ CREATE TABLE IF NOT EXISTS `port_mapping` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID', `license_id` int NOT NULL COMMENT 'licenseID', `protocal` varchar(10) NOT NULL DEFAULT 'TCP' COMMENT '协议', - `subdomain` varchar(50) DEFAULT NULL COMMENT '子域名(仅HTTP时有效)', `server_port` int NOT NULL COMMENT '服务端端口', `client_ip` varchar(20) NOT NULL COMMENT '客户端IP', `client_port` int NOT NULL COMMENT '客户端端口', @@ -119,6 +118,34 @@ CREATE TABLE IF NOT EXISTS `port_mapping` ( PRIMARY KEY (`id`), KEY `I_port_mapping_server_port` (`server_port`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +#域名表 +CREATE TABLE IF NOT EXISTS `domain_name` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `domain` varchar(50) NOT NULL COMMENT '域名', + `user_id` int NOT NULL COMMENT '用户Id', + `jks` BLOB DEFAULT NULL COMMENT 'jks文件', + `key_store_password` varchar(255) DEFAULT NULL COMMENT 'jks密码', + `is_default` int(2) NOT NULL COMMENT '是否开启默认域名(1、开启 2、关闭)', + `force_https` int(2) NOT NULL COMMENT '是否启用强制https(1、开启 2、关闭)', + `enable` int(2) NOT NULL COMMENT '是否启用(1、启用 2、禁用)', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_time` datetime NOT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE INDEX `I_domain_name_domain` (`domain` ASC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +#域名映射中间表 +CREATE TABLE IF NOT EXISTS `domain_port_mapping` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '创建时间', + `subdomain` varchar(50) NOT NULL COMMENT '子域名', + `domain_name_id` int NOT NULL COMMENT '域名Id', + `port_mapping_id` int NOT NULL COMMENT '端口映射Id', + PRIMARY KEY (`id`), + UNIQUE INDEX `I_unique_domain_port_mapping` (`domain_name_id` ASC, `subdomain` ASC), + INDEX `I_domain_port_mapping_domain_name_id` (`domain_name_id` ASC), + INDEX `I_domain_port_mapping_port_mapping_id` (`port_mapping_id` ASC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; #############################日志管理相关表############################# #用户登录记录表 CREATE TABLE IF NOT EXISTS `user_login_record` ( diff --git a/neutrino-proxy-server/src/main/resources/sql/mariadb/port_mapping.data.sql b/neutrino-proxy-server/src/main/resources/sql/mariadb/port_mapping.data.sql index 9063ecea..d1b7ef40 100644 --- a/neutrino-proxy-server/src/main/resources/sql/mariadb/port_mapping.data.sql +++ b/neutrino-proxy-server/src/main/resources/sql/mariadb/port_mapping.data.sql @@ -1,7 +1,7 @@ #port_mapping -INSERT INTO port_mapping(`id`, `license_id`, `server_port`, `protocal`, `subdomain`, `client_ip`, `client_port`, `is_online`, `enable`, `create_time`, `update_time`) VALUES -(1, 1, 9101, 'HTTP', 'test1', '127.0.0.1', 8080, 2, 1, now(), now()); -INSERT INTO port_mapping(`id`, `license_id`, `server_port`, `protocal`, `subdomain`, `client_ip`, `client_port`, `is_online`, `enable`, `create_time`, `update_time`) VALUES -(2, 1, 9102, 'TCP', '', '127.0.0.1', 3306, 2, 1, now(), now()); -INSERT INTO port_mapping(`id`, `license_id`, `server_port`, `protocal`, `subdomain`, `client_ip`, `client_port`, `is_online`, `enable`, `create_time`, `update_time`) VALUES -(3, 1, 9103, 'HTTP', 'test2', '127.0.0.1', 8081, 2, 1, now(), now()); +INSERT INTO port_mapping(`id`, `license_id`, `server_port`, `protocal`, `client_ip`, `client_port`, `is_online`, `enable`, `create_time`, `update_time`) VALUES +(1, 1, 9101, 'HTTP', '127.0.0.1', 8080, 2, 1, now(), now()); +INSERT INTO port_mapping(`id`, `license_id`, `server_port`, `protocal`, `client_ip`, `client_port`, `is_online`, `enable`, `create_time`, `update_time`) VALUES +(2, 1, 9102, 'TCP', '127.0.0.1', 3306, 2, 1, now(), now()); +INSERT INTO port_mapping(`id`, `license_id`, `server_port`, `protocal`, `client_ip`, `client_port`, `is_online`, `enable`, `create_time`, `update_time`) VALUES +(3, 1, 9103, 'HTTP', '127.0.0.1', 8081, 2, 1, now(), now()); diff --git a/neutrino-proxy-server/src/main/resources/sql/mysql/domain_name.data.sql b/neutrino-proxy-server/src/main/resources/sql/mysql/domain_name.data.sql new file mode 100644 index 00000000..e8e65699 --- /dev/null +++ b/neutrino-proxy-server/src/main/resources/sql/mysql/domain_name.data.sql @@ -0,0 +1,3 @@ +#域名表 +INSERT INTO domain_name(`id`, `domain`, `user_id`, `jks`, `key_store_password`, `is_default`, `force_https`, `enable`,`create_time`, `update_time`) VALUES +(1, 'default.fun', 1, null, null, 1, 2, 1, now(), now()); diff --git a/neutrino-proxy-server/src/main/resources/sql/mysql/init-structure.sql b/neutrino-proxy-server/src/main/resources/sql/mysql/init-structure.sql index d61fb9ac..65090ef0 100644 --- a/neutrino-proxy-server/src/main/resources/sql/mysql/init-structure.sql +++ b/neutrino-proxy-server/src/main/resources/sql/mysql/init-structure.sql @@ -102,7 +102,6 @@ CREATE TABLE IF NOT EXISTS `port_mapping` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID', `license_id` int NOT NULL COMMENT 'licenseID', `protocal` varchar(10) NOT NULL DEFAULT 'TCP' COMMENT '协议', - `subdomain` varchar(50) DEFAULT NULL COMMENT '子域名(仅HTTP时有效)', `server_port` int NOT NULL COMMENT '服务端端口', `client_ip` varchar(20) NOT NULL COMMENT '客户端IP', `client_port` int NOT NULL COMMENT '客户端端口', @@ -119,6 +118,34 @@ CREATE TABLE IF NOT EXISTS `port_mapping` ( PRIMARY KEY (`id`), KEY `I_port_mapping_server_port` (`server_port`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +#域名表 +CREATE TABLE IF NOT EXISTS `domain_name` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `domain` varchar(50) NOT NULL COMMENT '域名', + `user_id` int NOT NULL COMMENT '用户Id', + `jks` BLOB DEFAULT NULL COMMENT 'jks文件', + `key_store_password` varchar(255) DEFAULT NULL COMMENT 'jks密码', + `is_default` int(2) NOT NULL COMMENT '是否开启默认域名(1、开启 2、关闭)', + `force_https` int(2) NOT NULL COMMENT '是否启用强制https(1、开启 2、关闭)', + `enable` int(2) NOT NULL COMMENT '是否启用(1、启用 2、禁用)', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_time` datetime NOT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE INDEX `I_domain_name_domain` (`domain` ASC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +#域名映射中间表 +CREATE TABLE IF NOT EXISTS `domain_port_mapping` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '创建时间', + `subdomain` varchar(50) NOT NULL COMMENT '子域名', + `domain_name_id` int NOT NULL COMMENT '域名Id', + `port_mapping_id` int NOT NULL COMMENT '端口映射Id', + PRIMARY KEY (`id`), + UNIQUE INDEX `I_unique_domain_port_mapping` (`domain_name_id` ASC, `subdomain` ASC), + INDEX `I_domain_port_mapping_domain_name_id` (`domain_name_id` ASC), + INDEX `I_domain_port_mapping_port_mapping_id` (`port_mapping_id` ASC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; #############################日志管理相关表############################# #用户登录记录表 CREATE TABLE IF NOT EXISTS `user_login_record` ( diff --git a/neutrino-proxy-server/src/main/resources/sql/mysql/port_mapping.data.sql b/neutrino-proxy-server/src/main/resources/sql/mysql/port_mapping.data.sql index 9063ecea..d1b7ef40 100644 --- a/neutrino-proxy-server/src/main/resources/sql/mysql/port_mapping.data.sql +++ b/neutrino-proxy-server/src/main/resources/sql/mysql/port_mapping.data.sql @@ -1,7 +1,7 @@ #port_mapping -INSERT INTO port_mapping(`id`, `license_id`, `server_port`, `protocal`, `subdomain`, `client_ip`, `client_port`, `is_online`, `enable`, `create_time`, `update_time`) VALUES -(1, 1, 9101, 'HTTP', 'test1', '127.0.0.1', 8080, 2, 1, now(), now()); -INSERT INTO port_mapping(`id`, `license_id`, `server_port`, `protocal`, `subdomain`, `client_ip`, `client_port`, `is_online`, `enable`, `create_time`, `update_time`) VALUES -(2, 1, 9102, 'TCP', '', '127.0.0.1', 3306, 2, 1, now(), now()); -INSERT INTO port_mapping(`id`, `license_id`, `server_port`, `protocal`, `subdomain`, `client_ip`, `client_port`, `is_online`, `enable`, `create_time`, `update_time`) VALUES -(3, 1, 9103, 'HTTP', 'test2', '127.0.0.1', 8081, 2, 1, now(), now()); +INSERT INTO port_mapping(`id`, `license_id`, `server_port`, `protocal`, `client_ip`, `client_port`, `is_online`, `enable`, `create_time`, `update_time`) VALUES +(1, 1, 9101, 'HTTP', '127.0.0.1', 8080, 2, 1, now(), now()); +INSERT INTO port_mapping(`id`, `license_id`, `server_port`, `protocal`, `client_ip`, `client_port`, `is_online`, `enable`, `create_time`, `update_time`) VALUES +(2, 1, 9102, 'TCP', '127.0.0.1', 3306, 2, 1, now(), now()); +INSERT INTO port_mapping(`id`, `license_id`, `server_port`, `protocal`, `client_ip`, `client_port`, `is_online`, `enable`, `create_time`, `update_time`) VALUES +(3, 1, 9103, 'HTTP', '127.0.0.1', 8081, 2, 1, now(), now()); diff --git a/neutrino-proxy-server/src/main/resources/sql/mysql/update/UPDATE-20240808.SQL b/neutrino-proxy-server/src/main/resources/sql/mysql/update/UPDATE-20240808.SQL new file mode 100644 index 00000000..d18a4489 --- /dev/null +++ b/neutrino-proxy-server/src/main/resources/sql/mysql/update/UPDATE-20240808.SQL @@ -0,0 +1,30 @@ +#域名表 +CREATE TABLE IF NOT EXISTS `domain_name` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `domain` varchar(50) NOT NULL COMMENT '域名', + `user_id` int NOT NULL COMMENT '用户Id', + `jks` BLOB DEFAULT NULL COMMENT 'jks文件', + `key_store_password` varchar(255) DEFAULT NULL COMMENT 'jks密码', + `is_default` int(2) NOT NULL COMMENT '是否开启默认域名(1、开启 2、关闭)', + `force_https` int(2) NOT NULL COMMENT '是否启用强制https(1、开启 2、关闭)', + `enable` int(2) NOT NULL COMMENT '是否启用(1、启用 2、禁用)', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_time` datetime NOT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE INDEX `I_domain_name_domain` (`domain` ASC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +#域名映射中间表 +CREATE TABLE IF NOT EXISTS `domain_port_mapping` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '创建时间', + `subdomain` varchar(50) NOT NULL COMMENT '子域名', + `domain_name_id` int NOT NULL COMMENT '域名Id', + `port_mapping_id` int NOT NULL COMMENT '端口映射Id', + PRIMARY KEY (`id`), + UNIQUE INDEX `I_unique_domain_port_mapping` (`domain_name_id` ASC, `subdomain` ASC), + INDEX `I_domain_port_mapping_domain_name_id` (`domain_name_id` ASC), + INDEX `I_domain_port_mapping_port_mapping_id` (`port_mapping_id` ASC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +# port_mapping表删除字段 +ALTER TABLE `port_mapping` DROP COLUMN `subdomain`; \ No newline at end of file diff --git a/neutrino-proxy-vuepress/docs/01.使用教程/02.使用须知/01.管理后台.md b/neutrino-proxy-vuepress/docs/01.使用教程/02.使用须知/01.管理后台.md index cfb61744..836e213a 100644 --- a/neutrino-proxy-vuepress/docs/01.使用教程/02.使用须知/01.管理后台.md +++ b/neutrino-proxy-vuepress/docs/01.使用教程/02.使用须知/01.管理后台.md @@ -21,6 +21,7 @@ article: false - 默认所有代理没有限速 - 对License限速,将使得License下的所有端口映射都被限速 - 若License设置了限速,License下的某条映射也设置了限速,则对于该映射优先采用端口映射上的限速规则 +- 域名管理:用于管理服务器主域名和证书,用于支持域名映射。一个主域名域名可以创建多个域名映射(配置不同的子域名)。 diff --git a/neutrino-proxy-vuepress/docs/01.使用教程/02.使用须知/03.域名映射.md b/neutrino-proxy-vuepress/docs/01.使用教程/02.使用须知/03.域名映射.md index e88e3b98..9a48dcae 100644 --- a/neutrino-proxy-vuepress/docs/01.使用教程/02.使用须知/03.域名映射.md +++ b/neutrino-proxy-vuepress/docs/01.使用教程/02.使用须知/03.域名映射.md @@ -6,6 +6,7 @@ permalink: /pages/269a2e/ # 什么是域名映射? 域名映射是将指定端口映射绑定到某一个子域名上,通过直接访问域名的方式,达到访问被代理服务的目的。 +支持维护多个主域名并为多个主域名配置不同的子域名实现域名映射。 # 域名映射能解决什么问题? - 1、当服务端端口需要经常变动的时候,可以通过固定域名的方式,使得开发调试能够长期稳定的进行,不用每次更改访问地址。 @@ -17,8 +18,8 @@ permalink: /pages/269a2e/ ## 情况1:代理服务端使用80端口 - 1、DNS解析配置泛域名解析 proxy.asgc.fun 指向代理服务端IP -- 2、代理服务端app.yml配置域名:proxy.asgc.fun,部署 -- 3、服务端管理后台新建端口映射,协议选择:HTTP,设置好子域名,如:test1 +- 2、服务端管理后台域名管理新建域名,如:asgc.fun +- 3、服务端管理后台新建端口映射,协议选择:HTTP(S),选择主域名并设置好子域名,如:test1.proxy - 4、配置好客户端并启动,代理映射完成 - 5、通过访问: http://test1.proxy.asgc.fun 实现访问被代理服务 @@ -27,8 +28,8 @@ permalink: /pages/269a2e/ - 1、DNS解析配置泛域名解析 proxy.asgc.fun 指向代理服务端IP - 2、nginx 80端口下将 *.proxy.asgc.fun 请求转到82端口,并附带原有请求头(特别是`Host`) -- 3、代理服务端app.yml配置域名:proxy.asgc.fun,部署 -- 4、服务端管理后台新建端口映射,协议选择:HTTP,设置好子域名,如:test1 +- 3、服务端管理后台域名管理新建域名,如:asgc.fun +- 4、服务端管理后台新建端口映射,协议选择:HTTP(S),设置好子域名,如:test1.proxy - 5、配置好客户端并启动,代理映射完成 - 6、通过访问: http://test1.proxy.asgc.fun 实现访问被代理服务 @@ -36,6 +37,13 @@ permalink: /pages/269a2e/ 不行的。之所以能实现域名绑定,原因是HTTP请求附带了Host请求头,包含了请求的域名信息,代理服务端能根据请求的域名做分发。而TCP协议本身并不包含域名,因此无法实现。 所以,目前只有HTTP(涵盖HTTPS)协议能支持。 +# 域名映射支持HTTPS吗? +支持的。在服务端管理后台-域名管理中新建域名并上传域名对应的服务器证书,可以选择强制HTTPS,这样会拦截所有HTTP请求,只支持通过HTTPS访问。 +如果上传后的证书仍不支持HTTPS访问,请确保上传证书有效并且和配置的主域名或完整域名映射匹配。 + +# 上传服务器证书的文件有要求吗? +有的。目前只支持通过jks文件上传服务器证书,jks文件中只包含服务器证书(如:.cer文件),并设置密码。 + # 代理服务端使用非80端口时,nginx配置示例 ``` server { diff --git a/neutrino-proxy-vuepress/docs/01.使用教程/02.使用须知/04.HTTPS配置.md b/neutrino-proxy-vuepress/docs/01.使用教程/02.使用须知/04.HTTPS配置.md index e5b0cdab..10396d23 100644 --- a/neutrino-proxy-vuepress/docs/01.使用教程/02.使用须知/04.HTTPS配置.md +++ b/neutrino-proxy-vuepress/docs/01.使用教程/02.使用须知/04.HTTPS配置.md @@ -18,7 +18,8 @@ permalink: /pages/99a300/ ## HTTPS配置流程 - 按照域名映射流程,完成域名的配置 -- 在服务端配置域名的HTTPS证书信息(此处应该是通配符证书) +- 在服务端管理后台上传对应域名的HTTPS证书信息(此处应该是通配符证书) + ## HTTPS端口问题 - 与HTTP类似,HTTPS也有一个默认端口:443,默认情况下需要占用服务端443端口 diff --git a/neutrino-proxy-vuepress/docs/01.使用教程/02.使用须知/05.服务端配置.md b/neutrino-proxy-vuepress/docs/01.使用教程/02.使用须知/05.服务端配置.md index 941159f9..abae219d 100644 --- a/neutrino-proxy-vuepress/docs/01.使用教程/02.使用须知/05.服务端配置.md +++ b/neutrino-proxy-vuepress/docs/01.使用教程/02.使用须知/05.服务端配置.md @@ -1,12 +1,12 @@ --- title: 服务端配置 -date: 2023-05-27 11:41:44 +date: 2024-09-22 11:41:44 permalink: /pages/f2d0f1/ --- ::: tip -1.9.0版本,服务端配置格式有所调整。从之前的版本升级的,需要注意 +2.0.2版本,服务端配置格式有所调整。从之前的版本升级的,需要注意 ::: @@ -16,13 +16,12 @@ permalink: /pages/f2d0f1/ server: # 服务端web端口,用于支持HTTP接口,管理后台页面访问 port: ${WEB_PORT:8888} -# 日志级别 -solon.logging.logger: - "root": - level: info neutrino: proxy: + logger: + # 日志级别 + level: ${LOG_LEVEL:info} # 隧道相关配置-用于维持服务端与客户端的通信 tunnel: # 线程池相关配置,用于技术调优,可忽略 @@ -38,8 +37,6 @@ neutrino: jks-path: ${JKS_PATH:classpath:/test.jks} # 是否开启隧道传输报文日志(日志级别为debug时开启才有效) transfer-log-enable: ${TUNNEL_LOG:false} - # 是否开启心跳日志 - heartbeat-log-enable: ${HEARTBEAT_LOG:false} server: tcp: # 线程池相关配置,用于技术调优,可忽略 @@ -49,11 +46,6 @@ neutrino: http-proxy-port: ${HTTP_PROXY_PORT:80} # https代理端口,默认443 (需要配置域名、证书) https-proxy-port: ${HTTPS_PROXY_PORT:443} - # 如果不配置,则不支持域名映射 - domain-name: ${DOMAIN_NAME:} - # https证书配置 - key-store-password: ${HTTPS_STORE_PASS:} - jks-path: ${HTTPS_JKS_PATH:} # 是否开启代理服务报文日志(日志级别为debug时开启才有效) transfer-log-enable: ${SERVER_LOG:false} udp: @@ -64,7 +56,7 @@ neutrino: transfer-log-enable: ${SERVER_LOG:false} data: db: - # 数据库类型,目前支持h2、mysql、mariadb + # 数据库类型,目前支持sqlite、mysql、mariadb type: ${DB_TYPE:h2} # 数据库连接URL url: ${DB_URL:jdbc:h2:file:./data/db;MODE=MySQL;AUTO_SERVER=TRUE} diff --git a/neutrino-proxy-vuepress/docs/05.更新记录/01.版本记录/98.2.x.md b/neutrino-proxy-vuepress/docs/05.更新记录/01.版本记录/98.2.x.md index cdc1c3c7..8318f222 100644 --- a/neutrino-proxy-vuepress/docs/05.更新记录/01.版本记录/98.2.x.md +++ b/neutrino-proxy-vuepress/docs/05.更新记录/01.版本记录/98.2.x.md @@ -3,6 +3,15 @@ title: 2.x date: 2023-10-26 17:43:43 permalink: /pages/ff3519/ --- +## 2.0.2 +- 新增后台域名管理,⽀持新增、修改、删除、禁⽤主域名,支持添加多级域名。 +- 新增页面上传域名对应的SSL证书、⽀持设置强制HTTPS、⽀持域名映射⾃动加载对应的服务器证书。 +- 新增端⼝映射选择切换不同的主域名,⽀持绑定多个⼦域名,单个端⼝⽀持绑定多个不同的主域名。 +- 升级须知: + - **域名和SSL证书的配置由配置文件配置改为后台域名管理页面配置。** + - 去掉了配置文件声明服务器端域名和证书的方式,该为通过页面进行管理。请注意代码更新和数据迁移。 + - 数据库新增`域名表`和`域名映射中间表`,修改了`端口映射表`,涉及到表结构变更,执行[增量SQL](https://gitee.com/dromara/neutrino-proxy/blob/master/neutrino-proxy-server/src/main/resources/sql/mysql/update/UPDATE-20240808.SQL) + - 配置文件做了较大调整,请参照官网使用须知中的`服务端配置`、`客户端配置`进行更新 ## 2.0.1 - jdk版本升级为21