Compare commits

...
21 Commits
Author SHA1 Message Date
aoshiguchen 7255701084 workflow调整 2023-12-18 22:31:26 +08:00
aoshiguchen 99b3b673f1 workflow调整. 2023-12-18 21:45:26 +08:00
aoshiguchen f6f6017f8a 更新版本日志 2023-12-18 19:20:22 +08:00
aoshiguchen fc0830acfe 解决使用映射域名上传文件连接断开的问题 2023-12-18 18:39:23 +08:00
aoshiguchen 9baee2c909 调整workflow. 2023-12-18 16:43:19 +08:00
aoshiguchen 6558a8c327 调整workflow 2023-12-18 16:27:22 +08:00
aoshiguchen ffa8ee502c Merge remote-tracking branch 'origin/dev' into dev 2023-12-18 15:44:29 +08:00
aoshiguchen 4c993316de . 2023-12-18 15:44:21 +08:00
songyinyin d451424de8 Github Action: fix 前端打包 2023-12-18 15:35:49 +08:00
songyinyin 754070e795 Github Action: 临时上传构建物 2023-12-18 14:43:54 +08:00
aoshiguchen b21fd8e2d9 注释无用测试类. 2023-12-18 13:50:28 +08:00
aoshiguchen 4f5fc5dfbc 注释无用测试类 2023-12-18 13:39:06 +08:00
aoshiguchen d1eba19cb9 注释无用测试类 2023-12-18 13:38:37 +08:00
songyinyin efb3a7015d Merge remote-tracking branch 'origin/dev' into dev 2023-12-18 12:00:16 +08:00
songyinyin 47efe37a0e Github Action: 构建物的版本号使用 pom 文件中定义的 2023-12-18 12:00:06 +08:00
aoshiguchen 0ad05e7dc8 端口映射分页优化,解决license被删除后,端口映射分页报错的问题 2023-12-18 11:53:44 +08:00
aoshiguchen 9576707660 限速描述支持小数,如1.5M、3.7M 2023-12-18 11:36:27 +08:00
aoshiguchen 168acd6bfe 更新版本日志. 2023-12-18 11:30:09 +08:00
aoshiguchen ad32e36ba2 . 2023-12-18 10:47:55 +08:00
aoshiguchen 00fd0bab84 限速相关处理优化 2023-12-16 16:07:47 +08:00
songyinyin 8f90cf461d 原生编译:注册 netty 在 jdk 21 中新使用的类 2023-12-15 18:18:00 +08:00
14 changed files with 131 additions and 108 deletions
+33 -12
View File
@@ -12,7 +12,7 @@ jobs:
fail-fast: false
matrix:
# see: https://docs.github.com/zh/actions/using-jobs/choosing-the-runner-for-a-job
# os: ['ubuntu-20.04', 'macos-12']
# os: ['ubuntu-20.04', 'macos-12']
os: ['ubuntu-20.04']
name: build - ${{ matrix.os }}
runs-on: ${{ matrix.os }}
@@ -30,7 +30,7 @@ jobs:
run: |
cd neutrino-proxy-admin
npm install
npm run build:docker·
npm run build:docker
cp -rf ./dist/ ./../neutrino-proxy-server/src/main/resources/static/
- name: GitHub Action for GraalVM JDK 21
uses: graalvm/setup-graalvm@v1
@@ -45,34 +45,55 @@ jobs:
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Set vars
shell: bash
run: |
OS=$(echo '${{ runner.os }}' | awk '{print tolower($0)}')
[[ $OS == 'ubuntu' ]] && echo "OS=linux" >> $GITHUB_ENV || echo "OS=$OS" >> $GITHUB_ENV
[[ $OS == 'macos' ]] && echo "OS=darwin" >> $GITHUB_ENV || echo "OS=$OS" >> $GITHUB_ENV
echo "VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_ENV
- name: Build with Maven (neutrino-proxy-server)
run: |
mvn clean install -pl neutrino-proxy-core -am -DskipTests --no-transfer-progress
cd neutrino-proxy-server
mvn clean native:compile -P native --file pom.xml --no-transfer-progress
chmod +x target/neutrino-proxy-server
cp target/neutrino-proxy-server ./../neutrino-proxy-server-${{ github.ref_name }}
cp target/classes/app-copy.yml ./../neutrino-proxy-server-${{ github.ref_name }}/app.yml
mkdir ./../neutrino-proxy-server-${{ env.OS }}-${{ env.VERSION }}-native
cp target/neutrino-proxy-server ./../neutrino-proxy-server-${{ env.OS }}-${{ env.VERSION }}-native/neutrino-proxy-server
cp target/classes/app-copy.yml ./../neutrino-proxy-server-${{ env.OS }}-${{ env.VERSION }}-native/app.yml
- name: Archive zip (neutrino-proxy-server)
uses: thedoctor0/zip-release@master
with:
type: 'zip'
path: 'neutrino-proxy-server-${{ github.ref_name }}'
filename: neutrino-proxy-server-${{ matrix.os }}-${{ github.ref_name }}.zip
path: 'neutrino-proxy-server-${{ env.OS }}-${{ env.VERSION }}-native'
filename: neutrino-proxy-server-${{ env.OS }}-${{ env.VERSION }}-native.zip
- name: Build with Maven (neutrino-proxy-client)
run: |
cd neutrino-proxy-client
mvn clean package --file pom.xml --no-transfer-progress
mvn clean native:compile -P native --file pom.xml --no-transfer-progress
chmod +x target/neutrino-proxy-client
cp target/neutrino-proxy-client ./../neutrino-proxy-client-${{ github.ref_name }}
cp target/classes/app-copy.yml ./../neutrino-proxy-client-${{ github.ref_name }}/app.yml
mkdir ./../neutrino-proxy-client-${{ env.OS }}-${{ env.VERSION }}-native
cp target/neutrino-proxy-client ./../neutrino-proxy-client-${{ env.OS }}-${{ env.VERSION }}-native/neutrino-proxy-client
cp target/classes/app-copy.yml ./../neutrino-proxy-client-${{ env.OS }}-${{ env.VERSION }}-native/app.yml
- name: Archive zip (neutrino-proxy-client)
uses: thedoctor0/zip-release@master
with:
type: 'zip'
path: 'neutrino-proxy-client-${{ github.ref_name }}'
filename: neutrino-proxy-client-${{ matrix.os }}-${{ github.ref_name }}.zip
path: 'neutrino-proxy-client-${{ env.OS }}-${{ env.VERSION }}-native'
filename: neutrino-proxy-client-${{ env.OS }}-${{ env.VERSION }}-native.zip
# 上传构建产物
- name: Upload artifact
uses: actions/[email protected]
with:
name: neutrino-proxy-server-client-${{ env.OS }}-${{ env.VERSION }}
path: |
neutrino-proxy-server/target/neutrino-proxy-server.jar
neutrino-proxy-client/target/neutrino-proxy-client.jar
neutrino-proxy-server-${{ env.OS }}-${{ env.VERSION }}-native.zip
neutrino-proxy-client-${{ env.OS }}-${{ env.VERSION }}-native.zip
# 保存时间,默认 90 天
retention-days: 5
# GitHub 上创建 release
- name: Release
uses: softprops/action-gh-release@v1
@@ -81,7 +102,7 @@ jobs:
files: |
neutrino-proxy-server/target/neutrino-proxy-server.jar
neutrino-proxy-client/target/neutrino-proxy-client.jar
neutrino-proxy-server-${{ matrix.os }}-${{ github.ref_name }}.zip
neutrino-proxy-client-${{ matrix.os }}-${{ github.ref_name }}.zip
neutrino-proxy-server-${{ env.OS }}-${{ env.VERSION }}-native.zip
neutrino-proxy-client-${{ env.OS }}-${{ env.VERSION }}-native.zip
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
+31 -8
View File
@@ -44,32 +44,55 @@ jobs:
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Set vars
shell: bash
run: |
OS=$(echo '${{ runner.os }}' | awk '{print tolower($0)}')
[[ $OS == 'ubuntu' ]] && echo "OS=linux" >> $GITHUB_ENV || echo "OS=$OS" >> $GITHUB_ENV
[[ $OS == 'macos' ]] && echo "OS=darwin" >> $GITHUB_ENV || echo "OS=$OS" >> $GITHUB_ENV
echo "VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_ENV
- name: Build with Maven (neutrino-proxy-server)
run: |
set MAVEN_OPTS="-Duser.language=UTF-8 -Dfile.encoding=UTF-8"
mvn clean install -pl neutrino-proxy-core -am -DskipTests --no-transfer-progress
cd neutrino-proxy-server
mvn clean native:compile -P native --file pom.xml --no-transfer-progress
cp target/neutrino-proxy-server.exe ./../neutrino-proxy-server.exe
mkdir ./../neutrino-proxy-server-${{ env.OS }}-${{ env.VERSION }}-native
cp target/neutrino-proxy-server.exe ./../neutrino-proxy-server-${{ env.OS }}-${{ env.VERSION }}-native/neutrino-proxy-server.exe
cp target/classes/app-copy.yml ./../neutrino-proxy-server-${{ env.OS }}-${{ env.VERSION }}-native/app.yml
- name: Archive zip (neutrino-proxy-server)
uses: thedoctor0/zip-release@master
with:
type: 'zip'
path: 'neutrino-proxy-server.exe'
filename: neutrino-proxy-server-${{ matrix.os }}-${{ github.ref_name }}.zip
path: 'neutrino-proxy-server-${{ env.OS }}-${{ env.VERSION }}-native'
filename: neutrino-proxy-server-${{ env.OS }}-${{ env.VERSION }}-native.zip
- name: Build with Maven (neutrino-proxy-client)
run: |
set MAVEN_OPTS="-Duser.language=UTF-8 -Dfile.encoding=UTF-8"
cd neutrino-proxy-client
mvn clean package --file pom.xml --no-transfer-progress
mvn clean native:compile -P native --file pom.xml --no-transfer-progress
cp target/neutrino-proxy-client.exe ./../neutrino-proxy-client.exe
mkdir ./../neutrino-proxy-client-${{ env.OS }}-${{ env.VERSION }}-native
cp target/neutrino-proxy-client.exe ./../neutrino-proxy-client-${{ env.OS }}-${{ env.VERSION }}-native/neutrino-proxy-client.exe
cp target/classes/app-copy.yml ./../neutrino-proxy-client-${{ env.OS }}-${{ env.VERSION }}-native/app.yml
- name: Archive zip (neutrino-proxy-client)
uses: thedoctor0/zip-release@master
with:
type: 'zip'
path: 'neutrino-proxy-client.exe'
filename: neutrino-proxy-client-${{ matrix.os }}-${{ github.ref_name }}.zip
path: 'neutrino-proxy-client-${{ env.OS }}-${{ env.VERSION }}-native'
filename: neutrino-proxy-client-${{ env.OS }}-${{ env.VERSION }}-native.zip
# 上传构建产物
- name: Upload artifact
uses: actions/[email protected]
with:
name: neutrino-proxy-server-client-${{ env.OS }}-${{ env.VERSION }}
path: |
neutrino-proxy-server/target/neutrino-proxy-server.jar
neutrino-proxy-client/target/neutrino-proxy-client.jar
neutrino-proxy-server-${{ env.OS }}-${{ env.VERSION }}-native.zip
neutrino-proxy-client-${{ env.OS }}-${{ env.VERSION }}-native.zip
# 保存时间,默认 90 天
retention-days: 5
# GitHub 上创建 release
- name: Release
uses: softprops/action-gh-release@v1
@@ -78,7 +101,7 @@ jobs:
files: |
neutrino-proxy-server/target/neutrino-proxy-server.jar
neutrino-proxy-client/target/neutrino-proxy-client.jar
neutrino-proxy-server-${{ matrix.os }}-${{ github.ref_name }}.zip
neutrino-proxy-client-${{ matrix.os }}-${{ github.ref_name }}.zip
neutrino-proxy-server-${{ env.OS }}-${{ env.VERSION }}-native.zip
neutrino-proxy-client-${{ env.OS }}-${{ env.VERSION }}-native.zip
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
+4 -4
View File
@@ -75,7 +75,7 @@
## 2.0.1
- jdk版本升级为21
- native部署,后台新增提交到后端,数据库乱码问题
- 解决HTTP(S)映射时使用tcp端口访问正常,使用域名访问偶现一直loading的问题
- [ ] 支持被代理服务获取访问者真实ip
- [ ] 支持对用户、license限速
- 新增安全组模块,支持黑名单、白名单限制
- 支持对用户、license限速
- 修复HTTP(S)映射时使用tcp端口访问正常,使用域名访问偶现一直loading的问题
- 修复HTTP(S)映射时使用映射的域名上传文件时,连接中断的问题
@@ -50,6 +50,8 @@ public interface Constants {
AttributeKey<InetSocketAddress> SENDER = AttributeKey.newInstance("sender");
AttributeKey<Integer> SERVER_PORT = AttributeKey.newInstance("serverPort");
AttributeKey<String> REAL_REMOTE_IP = AttributeKey.newInstance("realRemoteIp");
AttributeKey<Boolean> FLOW_LIMITER_FLAG = AttributeKey.newInstance("flowLimiterFlag");
@@ -8,6 +8,7 @@ import org.noear.solon.aot.RuntimeNativeRegistrar;
import org.noear.solon.aot.hint.MemberCategory;
import org.noear.solon.core.AppContext;
import java.nio.ByteBuffer;
import java.util.Set;
/**
@@ -22,6 +23,11 @@ public class NeutrinoCoreRuntimeNativeRegistrar implements RuntimeNativeRegistra
metadata.registerReflection(clazz, MemberCategory.INVOKE_DECLARED_METHODS);
}
Set<Class<?>> byteBufferClasses = ClassUtil.scanPackageBySuper("java", ByteBuffer.class);
for (Class<?> clazz : byteBufferClasses) {
metadata.registerReflection(clazz, MemberCategory.INVOKE_DECLARED_METHODS);
}
metadata.registerReflection(ProxyMessage.class, MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
metadata.registerReflection(ProxyMessage.UdpBaseInfo.class, MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
@@ -77,8 +77,8 @@ public class ProxyConfiguration implements LifecycleBean {
}
ch.pipeline().addFirst(new BytesMetricsHandler());
// ch.pipeline().addLast(new ChannelTrafficShapingHandler(1024 * 1024 * 20, 1024 * 1024 * 20, 100, 20000));
ch.pipeline().addLast("security", new TcpVisitorSecurityChannelHandler());
ch.pipeline().addLast(new VisitorFlowLimiterChannelHandler());
ch.pipeline().addLast(new TcpVisitorSecurityChannelHandler());
ch.pipeline().addLast("flowLimiter", new VisitorFlowLimiterChannelHandler());
ch.pipeline().addLast(new TcpVisitorChannelHandler());
}
});
@@ -116,8 +116,8 @@ public class ProxyConfiguration implements LifecycleBean {
if (null != proxyConfig.getServer().getUdp().getTransferLogEnable() && proxyConfig.getServer().getUdp().getTransferLogEnable()) {
ch.pipeline().addFirst(new LoggingHandler(UdpVisitorChannelHandler.class));
}
pipeline.addLast(udpServerWorkerGroup, "security", new UdpVisitorSecurityChannelHandler());
ch.pipeline().addLast(new VisitorFlowLimiterChannelHandler());
pipeline.addLast(udpServerWorkerGroup, new UdpVisitorSecurityChannelHandler());
ch.pipeline().addLast("flowLimiter", new VisitorFlowLimiterChannelHandler());
pipeline.addLast(udpServerWorkerGroup, new UdpVisitorChannelHandler());
}
});
@@ -48,8 +48,8 @@ public class HttpProxy implements EventListener<AppLoadEndEvent> {
ch.pipeline().addFirst(new LoggingHandler(HttpProxy.class));
}
ch.pipeline().addFirst(new BytesMetricsHandler());
ch.pipeline().addLast("security", new HttpVisitorSecurityChannelHandler(proxyConfig.getServer().getTcp().getDomainName()));
ch.pipeline().addLast(new VisitorFlowLimiterChannelHandler());
ch.pipeline().addLast(new HttpVisitorSecurityChannelHandler(proxyConfig.getServer().getTcp().getDomainName()));
ch.pipeline().addLast("flowLimiter",new VisitorFlowLimiterChannelHandler());
ch.pipeline().addLast(new HttpVisitorChannelHandler());
}
});
@@ -57,8 +57,8 @@ public class HttpsProxy implements EventListener<AppLoadEndEvent> {
}
ch.pipeline().addLast(createSslHandler());
ch.pipeline().addFirst(new BytesMetricsHandler());
ch.pipeline().addLast("security", new HttpVisitorSecurityChannelHandler(proxyConfig.getServer().getTcp().getDomainName()));
ch.pipeline().addLast(new VisitorFlowLimiterChannelHandler());
ch.pipeline().addLast(new HttpVisitorSecurityChannelHandler(proxyConfig.getServer().getTcp().getDomainName()));
ch.pipeline().addLast("flowLimiter",new VisitorFlowLimiterChannelHandler());
ch.pipeline().addLast(new HttpVisitorChannelHandler());
}
});
@@ -39,47 +39,52 @@ public class HttpVisitorSecurityChannelHandler extends ChannelInboundHandlerAdap
}
ByteBuf buf = (ByteBuf) msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
// 获取Host请求头
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
Integer serverPort = ProxyUtil.getServerPortBySubdomain(subdomain);
Integer serverPort = ctx.channel().attr(Constants.SERVER_PORT).get();
if (null == serverPort) {
ctx.channel().close();
return;
}
// 获取Host请求头
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String httpContent = new String(bytes);
String host = HttpUtil.getHostIgnorePort(httpContent);
// 判断IP是否在该端口绑定的安全组允许的规则内
String ip = IpUtil.getRealRemoteIp(httpContent);
if (ip == null) {
ip = IpUtil.getRemoteIp(ctx);
}
if (!securityGroupService.judgeAllow(ip, portMappingService.getSecurityGroupIdByMappingPort(serverPort))) {
// 不在安全组规则放行范围内
ctx.channel().close();
return;
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);
if (null == serverPort) {
ctx.channel().close();
return;
}
// 判断IP是否在该端口绑定的安全组允许的规则内
String ip = IpUtil.getRealRemoteIp(httpContent);
if (ip == null) {
ip = IpUtil.getRemoteIp(ctx);
}
if (!securityGroupService.judgeAllow(ip, portMappingService.getSecurityGroupIdByMappingPort(serverPort))) {
// 不在安全组规则放行范围内
ctx.channel().close();
return;
}
ctx.channel().attr(Constants.REAL_REMOTE_IP).set(ip);
ctx.channel().attr(Constants.SERVER_PORT).set(serverPort);
}
// 继续传播
ctx.channel().attr(Constants.SERVER_PORT).set(serverPort);
buf.resetReaderIndex();
ctx.fireChannelRead(buf);
}
@@ -18,8 +18,6 @@ import org.noear.solon.Solon;
*/
@Slf4j
public class VisitorFlowLimiterChannelHandler extends ChannelInboundHandlerAdapter {
private final LicenseService licenseService = Solon.context().getBean(LicenseService.class);
private final PortMappingService portMappingService = Solon.context().getBean(PortMappingService.class);
@Override
@@ -38,7 +36,7 @@ public class VisitorFlowLimiterChannelHandler extends ChannelInboundHandlerAdapt
}
if (null != upLimitRate || null != downLimitRate) {
// 如果不全为空,则需要做限速
ctx.pipeline().addAfter("security", "trafficShaping", new ChannelTrafficShapingHandler(downLimitRate == null ? 0 : downLimitRate, upLimitRate == null ? 0 : upLimitRate, 100, 600000));
ctx.pipeline().addAfter("flowLimiter", "trafficShaping", new ChannelTrafficShapingHandler(downLimitRate == null ? 0 : downLimitRate, upLimitRate == null ? 0 : upLimitRate, 100, 600000));
}
// 每个连接第一次处理之后。无论是否限速,该连接后续都不在处理,避免频繁执行影响性能
@@ -138,7 +138,9 @@ public class PortMappingService implements LifecycleBean {
}
});
//sorted [userId asc] [licenseId asc] [createTime asc]
respList = respList.stream().sorted(Comparator.comparing(PortMappingListRes::getUserId).thenComparing(PortMappingListRes::getLicenseId).thenComparing(PortMappingListRes::getCreateTime)).collect(Collectors.toList());
respList = respList.stream()
.filter(e -> null != e.getUserId())
.sorted(Comparator.comparing(PortMappingListRes::getUserId).thenComparing(PortMappingListRes::getLicenseId).thenComparing(PortMappingListRes::getCreateTime)).collect(Collectors.toList());
return PageInfo.of(respList, page.getTotal(), pageQuery.getCurrent(), pageQuery.getSize());
}
@@ -15,7 +15,7 @@ public class StringUtil {
private static final Integer BYTES_MUL_GB = BYTES_MUL_MB * 1024;
private static final String[] BYTES_UNIT_STR = {"B", "K", "KB", "M", "MB", "G", "GB"};
private static final Integer[] BYTES_UNIT_MUL = {1, BYTES_MUL_KB, BYTES_MUL_KB, BYTES_MUL_MB, BYTES_MUL_MB, BYTES_MUL_GB, BYTES_MUL_GB};
private static final String BYTES_DESC_REGEX = "\\s*(\\d+)\\s*(B|K|KB|M|MB|G|GB)\\s*";
private static final String BYTES_DESC_REGEX = "\\s*(\\d+\\.*\\d*)\\s*(B|K|KB|M|MB|G|GB)\\s*";
private static final Pattern BYTES_DESC_PATTERN = Pattern.compile(BYTES_DESC_REGEX);
/**
@@ -46,14 +46,14 @@ public class StringUtil {
if (!found) {
return null;
}
Integer n = Integer.parseInt(matcher.group(1));
Double n = Double.parseDouble(matcher.group(1));
String unit = matcher.group(2);
Integer unitIndex = getBytesUnitIndex(unit);
if (null == unitIndex) {
return null;
}
return (long)n * BYTES_UNIT_MUL[unitIndex];
return (long)(n * BYTES_UNIT_MUL[unitIndex]);
} catch (Exception e) {
// ignore
}
@@ -1,19 +0,0 @@
package org.dromara.neutrinoproxy.server;
import org.dromara.neutrinoproxy.core.util.NeutrinoProxyVersion;
import org.dromara.neutrinoproxy.server.app.AppBaseTest;
import org.junit.Assert;
import org.junit.Test;
/**
* @author songyinyin
* @since 2023/10/31 11:51
*/
public class VersionTest extends AppBaseTest {
@Test
public void testVersion() {
Assert.assertNotNull(NeutrinoProxyVersion.getVersion());
System.out.println("version: " + NeutrinoProxyVersion.getVersion());
}
}
@@ -1,15 +0,0 @@
package org.dromara.neutrinoproxy.server.app;
import org.dromara.neutrinoproxy.server.ProxyServer;
import org.junit.runner.RunWith;
import org.noear.solon.test.SolonJUnit4ClassRunner;
import org.noear.solon.test.SolonTest;
/**
* @author songyinyin
* @since 2023/10/31 11:56
*/
@SolonTest(value = ProxyServer.class, properties = "server.port=18888")
@RunWith(SolonJUnit4ClassRunner.class)
public class AppBaseTest {
}