基础包名改为org.dromara.neutrinoproxy
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
package org.dromara.neutrinoproxy.client;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.noear.solon.Solon;
|
||||
import org.noear.solon.annotation.SolonMain;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author: aoshiguchen
|
||||
* @date: 2022/6/16
|
||||
*/
|
||||
@SolonMain
|
||||
public class ProxyClient {
|
||||
|
||||
public static void main(String[] args) {
|
||||
Solon.start(ProxyClient.class, args, app -> {
|
||||
setAlias("neutrino.proxy.client.serverIp", "serverIp");
|
||||
setAlias("neutrino.proxy.client.serverPort", "serverPort");
|
||||
setAlias("neutrino.proxy.client.sslEnable", "sslEnable");
|
||||
setAlias("neutrino.proxy.client.jksPath", "jksPath");
|
||||
setAlias("neutrino.proxy.client.keyStorePassword", "keyStorePassword");
|
||||
setAlias("neutrino.proxy.client.licenseKey", "licenseKey");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 别名处理,支持较短的启动参数名
|
||||
* @param key
|
||||
* @param alias
|
||||
*/
|
||||
private static void setAlias(String key, String alias) {
|
||||
String val = Solon.cfg().argx().get(alias);
|
||||
if (StrUtil.isNotBlank(val)) {
|
||||
Solon.cfg().put(key, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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.client.config;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author: aoshiguchen
|
||||
* @date: 2022/10/18
|
||||
*/
|
||||
@Data
|
||||
public class CustomConfig {
|
||||
private String jksPath;
|
||||
private String serverIp;
|
||||
private Integer serverPort;
|
||||
private Boolean sslEnable;
|
||||
private String licenseKey;
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package org.dromara.neutrinoproxy.client.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.noear.solon.annotation.Component;
|
||||
import org.noear.solon.annotation.Inject;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author: aoshiguchen
|
||||
* @date: 2022/6/16
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
public class ProxyConfig {
|
||||
@Inject("${neutrino.proxy.protocol}")
|
||||
private Protocol protocol;
|
||||
@Inject("${neutrino.proxy.client}")
|
||||
private Client client;
|
||||
|
||||
@Data
|
||||
public static class Protocol {
|
||||
private Integer maxFrameLength;
|
||||
private Integer lengthFieldOffset;
|
||||
private Integer lengthFieldLength;
|
||||
private Integer initialBytesToStrip;
|
||||
private Integer lengthAdjustment;
|
||||
private Integer readIdleTime;
|
||||
private Integer writeIdleTime;
|
||||
private Integer allIdleTimeSeconds;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Client {
|
||||
private String keyStorePassword;
|
||||
private String jksPath;
|
||||
private String serverIp;
|
||||
private Integer serverPort;
|
||||
private Boolean sslEnable;
|
||||
private Integer obtainLicenseInterval;
|
||||
private String licenseKey;
|
||||
private Integer threadCount;
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package org.dromara.neutrinoproxy.client.config;
|
||||
|
||||
import org.dromara.neutrinoproxy.core.ProxyDataTypeEnum;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessage;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessageHandler;
|
||||
import org.dromara.neutrinoproxy.core.dispatcher.DefaultDispatcher;
|
||||
import org.dromara.neutrinoproxy.core.dispatcher.Dispatcher;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import org.noear.solon.Solon;
|
||||
import org.noear.solon.annotation.Bean;
|
||||
import org.noear.solon.annotation.Configuration;
|
||||
import org.noear.solon.core.bean.LifecycleBean;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 代理配置
|
||||
* @author: aoshiguchen
|
||||
* @date: 2022/10/8
|
||||
*/
|
||||
@Configuration
|
||||
public class ProxyConfiguration implements LifecycleBean {
|
||||
|
||||
@Override
|
||||
public void start() throws Throwable {
|
||||
List<ProxyMessageHandler> list = Solon.context().getBeansOfType(ProxyMessageHandler.class);
|
||||
Dispatcher<ChannelHandlerContext, ProxyMessage> dispatcher = new DefaultDispatcher<>("消息调度器", list,
|
||||
proxyMessage -> ProxyDataTypeEnum.of((int)proxyMessage.getType()) == null ?
|
||||
null : ProxyDataTypeEnum.of((int)proxyMessage.getType()).getName());
|
||||
Solon.context().wrapAndPut(Dispatcher.class, dispatcher);
|
||||
}
|
||||
|
||||
@Bean("bootstrap")
|
||||
public Bootstrap bootstrap() {
|
||||
return new Bootstrap();
|
||||
}
|
||||
|
||||
@Bean("realServerBootstrap")
|
||||
public Bootstrap realServerBootstrap() {
|
||||
return new Bootstrap();
|
||||
}
|
||||
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package org.dromara.neutrinoproxy.client.core;
|
||||
|
||||
import org.dromara.neutrinoproxy.client.util.ProxyUtil;
|
||||
import org.dromara.neutrinoproxy.core.Constants;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessage;
|
||||
import org.dromara.neutrinoproxy.core.dispatcher.Dispatcher;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.noear.solon.Solon;
|
||||
|
||||
/**
|
||||
* 处理与服务端之间的数据传输
|
||||
* @author: aoshiguchen
|
||||
* @date: 2022/6/16
|
||||
*/
|
||||
@Slf4j
|
||||
public class ClientChannelHandler extends SimpleChannelInboundHandler<ProxyMessage> {
|
||||
|
||||
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, ProxyMessage proxyMessage) throws Exception {
|
||||
if (ProxyMessage.TYPE_HEARTBEAT != proxyMessage.getType()) {
|
||||
log.info("recieved proxy message, type is {}", proxyMessage.getType());
|
||||
}
|
||||
Solon.context().getBean(Dispatcher.class).dispatch(ctx, proxyMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
|
||||
Channel realServerChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
|
||||
if (realServerChannel != null) {
|
||||
realServerChannel.config().setOption(ChannelOption.AUTO_READ, ctx.channel().isWritable());
|
||||
}
|
||||
|
||||
super.channelWritabilityChanged(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
||||
// 控制连接
|
||||
if (ProxyUtil.getCmdChannel() == ctx.channel()) {
|
||||
log.info("与服务端断开连接");
|
||||
ProxyUtil.setCmdChannel(null);
|
||||
ProxyUtil.clearRealServerChannels();
|
||||
} else {
|
||||
// 数据传输连接
|
||||
Channel realServerChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
|
||||
if (realServerChannel != null && realServerChannel.isActive()) {
|
||||
realServerChannel.close();
|
||||
}
|
||||
}
|
||||
|
||||
ProxyUtil.removeProxyChanel(ctx.channel());
|
||||
super.channelInactive(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
super.exceptionCaught(ctx, cause);
|
||||
cause.printStackTrace();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
||||
if(evt instanceof IdleStateEvent) {
|
||||
IdleStateEvent event = (IdleStateEvent)evt;
|
||||
switch (event.state()) {
|
||||
case READER_IDLE:
|
||||
// 读超时,断开连接
|
||||
log.info("读超时");
|
||||
ctx.channel().close();
|
||||
break;
|
||||
case WRITER_IDLE:
|
||||
ctx.channel().writeAndFlush(ProxyMessage.buildHeartbeatMessage());
|
||||
break;
|
||||
case ALL_IDLE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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.client.core;
|
||||
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author: aoshiguchen
|
||||
* @date: 2022/9/4
|
||||
*/
|
||||
public class CustomThreadFactory implements ThreadFactory {
|
||||
private final ThreadGroup group;
|
||||
private final AtomicInteger threadNumber = new AtomicInteger(1);
|
||||
private final String namePrefix;
|
||||
|
||||
public CustomThreadFactory(String prefix) {
|
||||
SecurityManager s = System.getSecurityManager();
|
||||
group = (s != null) ? s.getThreadGroup() :
|
||||
Thread.currentThread().getThreadGroup();
|
||||
namePrefix = prefix + "-thread-";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable r) {
|
||||
Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
|
||||
if (t.isDaemon()) {
|
||||
t.setDaemon(false);
|
||||
}
|
||||
if (t.getPriority() != Thread.NORM_PRIORITY) {
|
||||
t.setPriority(Thread.NORM_PRIORITY);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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.client.core;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author: aoshiguchen
|
||||
* @date: 2022/6/16
|
||||
*/
|
||||
public interface ProxyChannelBorrowListener {
|
||||
|
||||
void success(Channel channel);
|
||||
|
||||
void error(Throwable cause);
|
||||
|
||||
}
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
package org.dromara.neutrinoproxy.client.core;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.dromara.neutrinoproxy.client.config.ProxyConfig;
|
||||
import org.dromara.neutrinoproxy.client.util.ProxyUtil;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessage;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessageDecoder;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessageEncoder;
|
||||
import org.dromara.neutrinoproxy.core.util.FileUtil;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.handler.ssl.SslHandler;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.noear.solon.Solon;
|
||||
import org.noear.solon.annotation.Component;
|
||||
import org.noear.solon.annotation.Init;
|
||||
import org.noear.solon.annotation.Inject;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyStore;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 客户端服务
|
||||
* @author: aoshiguchen
|
||||
* @date: 2022/6/16
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ProxyClientService {
|
||||
@Inject
|
||||
private ProxyConfig proxyConfig;
|
||||
@Inject("bootstrap")
|
||||
private Bootstrap bootstrap;
|
||||
@Inject("realServerBootstrap")
|
||||
private Bootstrap realServerBootstrap;
|
||||
private volatile Channel channel;
|
||||
/**
|
||||
* 重连间隔(秒)
|
||||
*/
|
||||
private static final long RECONNECT_INTERVAL_SECONDS = 5;
|
||||
/**
|
||||
* 重连次数
|
||||
*/
|
||||
private volatile int reconnectCount = 0;
|
||||
/**
|
||||
* 启用重连服务
|
||||
*/
|
||||
private volatile boolean reconnectServiceEnable = false;
|
||||
/**
|
||||
* 重连服务执行器
|
||||
*/
|
||||
private static final ScheduledExecutorService reconnectExecutor = Executors.newSingleThreadScheduledExecutor(new CustomThreadFactory("ClientReconnect"));
|
||||
|
||||
@Init
|
||||
public void init() {
|
||||
this.reconnectExecutor.scheduleWithFixedDelay(this::reconnect, 0, RECONNECT_INTERVAL_SECONDS, TimeUnit.SECONDS);
|
||||
|
||||
NioEventLoopGroup workerGroup = new NioEventLoopGroup(proxyConfig.getClient().getThreadCount());
|
||||
realServerBootstrap.group(workerGroup);
|
||||
realServerBootstrap.channel(NioSocketChannel.class);
|
||||
realServerBootstrap.handler(new ChannelInitializer<SocketChannel>() {
|
||||
|
||||
@Override
|
||||
public void initChannel(SocketChannel ch) throws Exception {
|
||||
ch.pipeline().addLast(new RealServerChannelHandler());
|
||||
}
|
||||
});
|
||||
|
||||
bootstrap.group(workerGroup);
|
||||
bootstrap.channel(NioSocketChannel.class);
|
||||
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
|
||||
|
||||
@Override
|
||||
public void initChannel(SocketChannel ch) throws Exception {
|
||||
if (proxyConfig.getClient().getSslEnable()) {
|
||||
ch.pipeline().addLast(createSslHandler());
|
||||
}
|
||||
|
||||
ch.pipeline().addLast(new ProxyMessageDecoder(proxyConfig.getProtocol().getMaxFrameLength(),
|
||||
proxyConfig.getProtocol().getLengthFieldOffset(), proxyConfig.getProtocol().getLengthFieldLength(),
|
||||
proxyConfig.getProtocol().getLengthAdjustment(), proxyConfig.getProtocol().getInitialBytesToStrip()));
|
||||
ch.pipeline().addLast(new ProxyMessageEncoder());
|
||||
ch.pipeline().addLast(new IdleStateHandler(proxyConfig.getProtocol().getReadIdleTime(), proxyConfig.getProtocol().getWriteIdleTime(), proxyConfig.getProtocol().getAllIdleTimeSeconds()));
|
||||
ch.pipeline().addLast(new ClientChannelHandler());
|
||||
}
|
||||
});
|
||||
this.start();
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (StrUtil.isEmpty(proxyConfig.getClient().getServerIp())) {
|
||||
log.error("not found server-ip config.");
|
||||
Solon.stop();
|
||||
return;
|
||||
}
|
||||
if (null == proxyConfig.getClient().getServerPort()) {
|
||||
log.error("not found server-port config.");
|
||||
Solon.stop();
|
||||
return;
|
||||
}
|
||||
if (null != proxyConfig.getClient().getSslEnable() && proxyConfig.getClient().getSslEnable()
|
||||
&& StrUtil.isEmpty(proxyConfig.getClient().getJksPath())) {
|
||||
log.error("not found jks-path config.");
|
||||
Solon.stop();
|
||||
return;
|
||||
}
|
||||
if (StrUtil.isEmpty(proxyConfig.getClient().getLicenseKey())) {
|
||||
log.error("not found license-key config.");
|
||||
Solon.stop();
|
||||
return;
|
||||
}
|
||||
if (null == channel || !channel.isActive()) {
|
||||
try {
|
||||
connectProxyServer();
|
||||
} catch (Exception e) {
|
||||
log.error("client start error", e);
|
||||
}
|
||||
} else {
|
||||
channel.writeAndFlush(ProxyMessage.buildAuthMessage(proxyConfig.getClient().getLicenseKey()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接代理服务器
|
||||
*/
|
||||
private void connectProxyServer() throws InterruptedException {
|
||||
bootstrap.connect(proxyConfig.getClient().getServerIp(), proxyConfig.getClient().getServerPort())
|
||||
.addListener(new ChannelFutureListener() {
|
||||
|
||||
@Override
|
||||
public void operationComplete(ChannelFuture future) throws Exception {
|
||||
if (future.isSuccess()) {
|
||||
channel = future.channel();
|
||||
// 连接成功,向服务器发送客户端认证信息(licenseKey)
|
||||
ProxyUtil.setCmdChannel(future.channel());
|
||||
future.channel().writeAndFlush(ProxyMessage.buildAuthMessage(proxyConfig.getClient().getLicenseKey()));
|
||||
log.info("连接代理服务成功. channelId:{}", future.channel().id().asLongText());
|
||||
|
||||
reconnectServiceEnable = true;
|
||||
reconnectCount = 0;
|
||||
} else {
|
||||
log.info("连接代理服务失败!");
|
||||
}
|
||||
}
|
||||
}).sync();
|
||||
}
|
||||
|
||||
private ChannelHandler createSslHandler() {
|
||||
try {
|
||||
InputStream jksInputStream = FileUtil.getInputStream(proxyConfig.getClient().getJksPath());
|
||||
|
||||
SSLContext clientContext = SSLContext.getInstance("TLS");
|
||||
final KeyStore ks = KeyStore.getInstance("JKS");
|
||||
ks.load(jksInputStream, proxyConfig.getClient().getKeyStorePassword().toCharArray());
|
||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init(ks);
|
||||
TrustManager[] trustManagers = tmf.getTrustManagers();
|
||||
clientContext.init(null, trustManagers, null);
|
||||
|
||||
SSLEngine sslEngine = clientContext.createSSLEngine();
|
||||
sslEngine.setUseClientMode(true);
|
||||
|
||||
return new SslHandler(sslEngine);
|
||||
} catch (Exception e) {
|
||||
log.error("创建SSL处理器失败", e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected synchronized void reconnect() {
|
||||
if (!reconnectServiceEnable) {
|
||||
return;
|
||||
}
|
||||
if (null != channel && channel.isActive()) {
|
||||
return;
|
||||
}
|
||||
log.info("客户端重连 seq:{}", ++reconnectCount);
|
||||
try {
|
||||
connectProxyServer();
|
||||
} catch (Exception e) {
|
||||
log.error("重连异常", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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.client.core;
|
||||
|
||||
import org.dromara.neutrinoproxy.client.util.ProxyUtil;
|
||||
import org.dromara.neutrinoproxy.core.Constants;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessage;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
|
||||
/**
|
||||
* 处理与被代理客户端的数据传输
|
||||
* @author: aoshiguchen
|
||||
* @date: 2022/6/16
|
||||
*/
|
||||
public class RealServerChannelHandler extends SimpleChannelInboundHandler<ByteBuf> {
|
||||
|
||||
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
|
||||
Channel realServerChannel = ctx.channel();
|
||||
Channel proxyChannel = realServerChannel.attr(Constants.NEXT_CHANNEL).get();
|
||||
if (null == proxyChannel) {
|
||||
// 代理客户端连接断开
|
||||
ctx.channel().close();
|
||||
} else {
|
||||
byte[] bytes = new byte[buf.readableBytes()];
|
||||
buf.readBytes(bytes);
|
||||
String visitorId = ProxyUtil.getVisitorIdByRealServerChannel(realServerChannel);
|
||||
proxyChannel.writeAndFlush(ProxyMessage.buildTransferMessage(visitorId, bytes));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive(ChannelHandlerContext ctx) throws Exception {
|
||||
super.channelActive(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
||||
Channel realServerChannel = ctx.channel();
|
||||
String visitorId = ProxyUtil.getVisitorIdByRealServerChannel(realServerChannel);
|
||||
ProxyUtil.removeRealServerChannel(visitorId);
|
||||
Channel channel = realServerChannel.attr(Constants.NEXT_CHANNEL).get();
|
||||
if (channel != null) {
|
||||
channel.writeAndFlush(ProxyMessage.buildDisconnectMessage(visitorId));
|
||||
}
|
||||
|
||||
super.channelInactive(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
|
||||
Channel realServerChannel = ctx.channel();
|
||||
Channel proxyChannel = realServerChannel.attr(Constants.NEXT_CHANNEL).get();
|
||||
if (proxyChannel != null) {
|
||||
proxyChannel.config().setOption(ChannelOption.AUTO_READ, realServerChannel.isWritable());
|
||||
}
|
||||
|
||||
super.channelWritabilityChanged(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
super.exceptionCaught(ctx, cause);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package org.dromara.neutrinoproxy.client.handler;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import org.dromara.neutrinoproxy.core.Constants;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessage;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessageHandler;
|
||||
import org.dromara.neutrinoproxy.core.dispatcher.Match;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.noear.solon.annotation.Component;
|
||||
|
||||
/**
|
||||
* 认证信息处理器
|
||||
* @author: aoshiguchen
|
||||
* @date: 2022/9/4
|
||||
*/
|
||||
@Slf4j
|
||||
@Match(type = Constants.ProxyDataTypeName.AUTH)
|
||||
@Component
|
||||
public class ProxyMessageAuthHandler implements ProxyMessageHandler {
|
||||
@Override
|
||||
public void handle(ChannelHandlerContext context, ProxyMessage proxyMessage) {
|
||||
String info = proxyMessage.getInfo();
|
||||
JSONObject data = JSONObject.parseObject(info);
|
||||
Integer code = data.getInteger("code");
|
||||
log.info("认证结果:{}", info);
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package org.dromara.neutrinoproxy.client.handler;
|
||||
|
||||
import org.dromara.neutrinoproxy.client.config.ProxyConfig;
|
||||
import org.dromara.neutrinoproxy.client.core.ProxyChannelBorrowListener;
|
||||
import org.dromara.neutrinoproxy.client.util.ProxyUtil;
|
||||
import org.dromara.neutrinoproxy.core.Constants;
|
||||
import org.dromara.neutrinoproxy.core.ProxyDataTypeEnum;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessage;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessageHandler;
|
||||
import org.dromara.neutrinoproxy.core.dispatcher.Match;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.channel.*;
|
||||
import org.noear.solon.annotation.Component;
|
||||
import org.noear.solon.annotation.Inject;
|
||||
|
||||
/**
|
||||
* 连接信息处理器
|
||||
* @author: aoshiguchen
|
||||
* @date: 2022/6/16
|
||||
*/
|
||||
@Match(type = Constants.ProxyDataTypeName.CONNECT)
|
||||
@Component
|
||||
public class ProxyMessageConnectHandler implements ProxyMessageHandler {
|
||||
@Inject("bootstrap")
|
||||
private Bootstrap bootstrap;
|
||||
@Inject("realServerBootstrap")
|
||||
private Bootstrap realServerBootstrap;
|
||||
@Inject
|
||||
private ProxyConfig proxyConfig;
|
||||
|
||||
@Override
|
||||
public void handle(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
|
||||
final Channel cmdChannel = ctx.channel();
|
||||
final String visitorId = proxyMessage.getInfo();
|
||||
String[] serverInfo = new String(proxyMessage.getData()).split(":");
|
||||
String ip = serverInfo[0];
|
||||
int port = Integer.parseInt(serverInfo[1]);
|
||||
// 连接真实的、被代理的服务
|
||||
realServerBootstrap.connect(ip, port).addListener(new ChannelFutureListener() {
|
||||
|
||||
@Override
|
||||
public void operationComplete(ChannelFuture future) throws Exception {
|
||||
|
||||
// 连接后端服务器成功
|
||||
if (future.isSuccess()) {
|
||||
final Channel realServerChannel = future.channel();
|
||||
|
||||
realServerChannel.config().setOption(ChannelOption.AUTO_READ, false);
|
||||
|
||||
// 获取连接
|
||||
ProxyUtil.borrowProxyChanel(bootstrap, new ProxyChannelBorrowListener() {
|
||||
|
||||
@Override
|
||||
public void success(Channel channel) {
|
||||
// 连接绑定
|
||||
channel.attr(Constants.NEXT_CHANNEL).set(realServerChannel);
|
||||
realServerChannel.attr(Constants.NEXT_CHANNEL).set(channel);
|
||||
|
||||
// 远程绑定
|
||||
channel.writeAndFlush(ProxyMessage.buildConnectMessage(visitorId + "@" + proxyConfig.getClient().getLicenseKey()));
|
||||
|
||||
realServerChannel.config().setOption(ChannelOption.AUTO_READ, true);
|
||||
ProxyUtil.addRealServerChannel(visitorId, realServerChannel);
|
||||
ProxyUtil.setRealServerChannelVisitorId(realServerChannel, visitorId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(Throwable cause) {
|
||||
ProxyMessage proxyMessage = new ProxyMessage();
|
||||
proxyMessage.setType(ProxyMessage.TYPE_DISCONNECT);
|
||||
proxyMessage.setInfo(visitorId);
|
||||
cmdChannel.writeAndFlush(proxyMessage);
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
cmdChannel.writeAndFlush(ProxyMessage.buildDisconnectMessage(visitorId));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return ProxyDataTypeEnum.CONNECT.getDesc();
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package org.dromara.neutrinoproxy.client.handler;
|
||||
|
||||
import org.dromara.neutrinoproxy.client.util.ProxyUtil;
|
||||
import org.dromara.neutrinoproxy.core.Constants;
|
||||
import org.dromara.neutrinoproxy.core.ProxyDataTypeEnum;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessage;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessageHandler;
|
||||
import org.dromara.neutrinoproxy.core.dispatcher.Match;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import org.noear.solon.annotation.Component;
|
||||
|
||||
/**
|
||||
* 断开连接信息处理器
|
||||
* @author: aoshiguchen
|
||||
* @date: 2022/6/16
|
||||
*/
|
||||
@Match(type = Constants.ProxyDataTypeName.DISCONNECT)
|
||||
@Component
|
||||
public class ProxyMessageDisconnectHandler implements ProxyMessageHandler {
|
||||
|
||||
@Override
|
||||
public void handle(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
|
||||
Channel realServerChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
|
||||
if (null != realServerChannel) {
|
||||
ctx.channel().attr(Constants.NEXT_CHANNEL).remove();
|
||||
ProxyUtil.returnProxyChanel(ctx.channel());
|
||||
realServerChannel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return ProxyDataTypeEnum.DISCONNECT.getDesc();
|
||||
}
|
||||
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package org.dromara.neutrinoproxy.client.handler;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import org.dromara.neutrinoproxy.core.*;
|
||||
import org.dromara.neutrinoproxy.core.*;
|
||||
import org.dromara.neutrinoproxy.core.dispatcher.Match;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.noear.solon.annotation.Component;
|
||||
|
||||
/**
|
||||
* 异常信息处理器
|
||||
* @author: aoshiguchen
|
||||
* @date: 2022/6/16
|
||||
*/
|
||||
@Slf4j
|
||||
@Match(type = Constants.ProxyDataTypeName.ERROR)
|
||||
@Component
|
||||
public class ProxyMessageErrorHandler implements ProxyMessageHandler {
|
||||
|
||||
@Override
|
||||
public void handle(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
|
||||
log.info("异常信息: {}", proxyMessage.getInfo());
|
||||
JSONObject data = JSONObject.parseObject(proxyMessage.getInfo());
|
||||
Integer code = data.getInteger("code");
|
||||
if (ExceptionEnum.AUTH_FAILED.getCode().equals(code)) {
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return ProxyDataTypeEnum.DISCONNECT.getDesc();
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package org.dromara.neutrinoproxy.client.handler;
|
||||
|
||||
import org.dromara.neutrinoproxy.core.Constants;
|
||||
import org.dromara.neutrinoproxy.core.ProxyDataTypeEnum;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessage;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessageHandler;
|
||||
import org.dromara.neutrinoproxy.core.dispatcher.Match;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import org.noear.solon.annotation.Component;
|
||||
|
||||
/**
|
||||
* 传输信息处理器
|
||||
* @author: aoshiguchen
|
||||
* @date: 2022/6/16
|
||||
*/
|
||||
@Match(type = Constants.ProxyDataTypeName.TRANSFER)
|
||||
@Component
|
||||
public class ProxyMessageTransferHandler implements ProxyMessageHandler {
|
||||
|
||||
@Override
|
||||
public void handle(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
|
||||
Channel realServerChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
|
||||
if (realServerChannel != null) {
|
||||
ByteBuf buf = ctx.alloc().buffer(proxyMessage.getData().length);
|
||||
buf.writeBytes(proxyMessage.getData());
|
||||
realServerChannel.writeAndFlush(buf);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return ProxyDataTypeEnum.TRANSFER.getDesc();
|
||||
}
|
||||
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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.client.util;
|
||||
|
||||
import org.dromara.neutrinoproxy.client.core.ProxyChannelBorrowListener;
|
||||
import org.dromara.neutrinoproxy.core.Constants;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.util.AttributeKey;
|
||||
import org.noear.solon.Solon;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
/**
|
||||
* 代理工具
|
||||
* @author: aoshiguchen
|
||||
* @date: 2022/8/31
|
||||
*/
|
||||
public class ProxyUtil {
|
||||
private static final AttributeKey<Boolean> USER_CHANNEL_WRITEABLE = AttributeKey.newInstance("user_channel_writeable");
|
||||
|
||||
private static final AttributeKey<Boolean> CLIENT_CHANNEL_WRITEABLE = AttributeKey.newInstance("client_channel_writeable");
|
||||
|
||||
private static final int MAX_POOL_SIZE = 100;
|
||||
|
||||
private static Map<String, Channel> realServerChannels = new ConcurrentHashMap<String, Channel>();
|
||||
|
||||
private static ConcurrentLinkedQueue<Channel> proxyChannelPool = new ConcurrentLinkedQueue<Channel>();
|
||||
|
||||
private static volatile Channel cmdChannel;
|
||||
|
||||
public static void borrowProxyChanel(Bootstrap bootstrap, final ProxyChannelBorrowListener borrowListener) {
|
||||
Channel channel = proxyChannelPool.poll();
|
||||
if (null != channel) {
|
||||
borrowListener.success(channel);
|
||||
return;
|
||||
}
|
||||
|
||||
String serverIp = Solon.cfg().get("neutrino.proxy.client.server-ip");
|
||||
Integer serverPort = Solon.cfg().getInt("neutrino.proxy.client.server-port", 9000);
|
||||
bootstrap.connect(serverIp, serverPort).addListener((ChannelFutureListener) future -> {
|
||||
if (future.isSuccess()) {
|
||||
borrowListener.success(future.channel());
|
||||
} else {
|
||||
borrowListener.error(future.cause());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void returnProxyChanel(Channel proxyChanel) {
|
||||
if (proxyChannelPool.size() > MAX_POOL_SIZE) {
|
||||
proxyChanel.close();
|
||||
} else {
|
||||
proxyChanel.config().setOption(ChannelOption.AUTO_READ, true);
|
||||
proxyChanel.attr(Constants.NEXT_CHANNEL).remove();
|
||||
proxyChannelPool.offer(proxyChanel);
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeProxyChanel(Channel proxyChanel) {
|
||||
proxyChannelPool.remove(proxyChanel);
|
||||
}
|
||||
|
||||
public static void setCmdChannel(Channel cmdChannel) {
|
||||
ProxyUtil.cmdChannel = cmdChannel;
|
||||
}
|
||||
|
||||
public static Channel getCmdChannel() {
|
||||
return cmdChannel;
|
||||
}
|
||||
|
||||
public static void setRealServerChannelVisitorId(Channel realServerChannel, String visitorId) {
|
||||
realServerChannel.attr(Constants.VISITOR_ID).set(visitorId);
|
||||
}
|
||||
|
||||
public static String getVisitorIdByRealServerChannel(Channel realServerChannel) {
|
||||
return realServerChannel.attr(Constants.VISITOR_ID).get();
|
||||
}
|
||||
|
||||
public static Channel getRealServerChannel(String userId) {
|
||||
return realServerChannels.get(userId);
|
||||
}
|
||||
|
||||
public static void addRealServerChannel(String userId, Channel realServerChannel) {
|
||||
realServerChannels.put(userId, realServerChannel);
|
||||
}
|
||||
|
||||
public static Channel removeRealServerChannel(String userId) {
|
||||
return realServerChannels.remove(userId);
|
||||
}
|
||||
|
||||
public static boolean isRealServerReadable(Channel realServerChannel) {
|
||||
return realServerChannel.attr(CLIENT_CHANNEL_WRITEABLE).get() && realServerChannel.attr(USER_CHANNEL_WRITEABLE).get();
|
||||
}
|
||||
|
||||
public static void clearRealServerChannels() {
|
||||
Iterator<Map.Entry<String, Channel>> ite = realServerChannels.entrySet().iterator();
|
||||
while (ite.hasNext()) {
|
||||
Channel realServerChannel = ite.next().getValue();
|
||||
if (realServerChannel.isActive()) {
|
||||
realServerChannel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
}
|
||||
|
||||
realServerChannels.clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user