Repository: Pluto-Whong/natcross2
Branch: master
Commit: 505da5d623c9
Files: 94
Total size: 238.6 KB
Directory structure:
gitextract_mhw2jg3q/
├── .gitignore
├── LICENSE
├── README.md
├── VERSION.md
├── pom.xml
└── src/
├── main/
│ ├── java/
│ │ └── person/
│ │ └── pluto/
│ │ └── natcross2/
│ │ ├── ClientApp.java
│ │ ├── CommonConstants.java
│ │ ├── ServerApp.java
│ │ ├── api/
│ │ │ ├── IBelongControl.java
│ │ │ ├── IHttpRouting.java
│ │ │ ├── passway/
│ │ │ │ ├── SecretPassway.java
│ │ │ │ └── SimplePassway.java
│ │ │ ├── secret/
│ │ │ │ ├── AESSecret.java
│ │ │ │ └── ISecret.java
│ │ │ └── socketpart/
│ │ │ ├── AbsSocketPart.java
│ │ │ ├── HttpRouteSocketPart.java
│ │ │ ├── SecretSocketPart.java
│ │ │ └── SimpleSocketPart.java
│ │ ├── channel/
│ │ │ ├── Channel.java
│ │ │ ├── InteractiveChannel.java
│ │ │ ├── JsonChannel.java
│ │ │ ├── LengthChannel.java
│ │ │ ├── SecretInteractiveChannel.java
│ │ │ ├── SocketChannel.java
│ │ │ └── StringChannel.java
│ │ ├── clientside/
│ │ │ ├── ClientControlThread.java
│ │ │ ├── adapter/
│ │ │ │ ├── IClientAdapter.java
│ │ │ │ └── InteractiveSimpleClientAdapter.java
│ │ │ ├── config/
│ │ │ │ ├── AllSecretInteractiveClientConfig.java
│ │ │ │ ├── HttpRouteClientConfig.java
│ │ │ │ ├── IClientConfig.java
│ │ │ │ ├── InteractiveClientConfig.java
│ │ │ │ └── SecretInteractiveClientConfig.java
│ │ │ ├── handler/
│ │ │ │ ├── CommonReplyHandler.java
│ │ │ │ ├── IClientHandler.java
│ │ │ │ ├── ServerHeartHandler.java
│ │ │ │ └── ServerWaitClientHandler.java
│ │ │ └── heart/
│ │ │ ├── ClientHeartThread.java
│ │ │ └── IClientHeartThread.java
│ │ ├── common/
│ │ │ ├── CommonFormat.java
│ │ │ └── Optional.java
│ │ ├── executor/
│ │ │ ├── IExecutor.java
│ │ │ ├── NatcrossExecutor.java
│ │ │ └── SimpleExecutor.java
│ │ ├── model/
│ │ │ ├── HttpRoute.java
│ │ │ ├── InteractiveModel.java
│ │ │ ├── NatcrossResultModel.java
│ │ │ ├── SecretInteractiveModel.java
│ │ │ ├── enumeration/
│ │ │ │ ├── InteractiveTypeEnum.java
│ │ │ │ └── NatcrossResultEnum.java
│ │ │ └── interactive/
│ │ │ ├── ClientConnectModel.java
│ │ │ ├── ClientControlModel.java
│ │ │ └── ServerWaitModel.java
│ │ ├── nio/
│ │ │ ├── INioProcessor.java
│ │ │ ├── NioHallows.java
│ │ │ └── ProcesserHolder.java
│ │ ├── serverside/
│ │ │ ├── client/
│ │ │ │ ├── ClientServiceThread.java
│ │ │ │ ├── adapter/
│ │ │ │ │ ├── DefaultReadAheadPassValueAdapter.java
│ │ │ │ │ ├── IClientServiceAdapter.java
│ │ │ │ │ ├── PassValueNextEnum.java
│ │ │ │ │ └── ReadAheadPassValueAdapter.java
│ │ │ │ ├── config/
│ │ │ │ │ ├── IClientServiceConfig.java
│ │ │ │ │ ├── SecretSimpleClientServiceConfig.java
│ │ │ │ │ └── SimpleClientServiceConfig.java
│ │ │ │ ├── handler/
│ │ │ │ │ ├── DefaultInteractiveProcessHandler.java
│ │ │ │ │ ├── IPassValueHandler.java
│ │ │ │ │ └── InteractiveProcessHandler.java
│ │ │ │ └── process/
│ │ │ │ ├── ClientConnectProcess.java
│ │ │ │ ├── ClientControlProcess.java
│ │ │ │ └── IProcess.java
│ │ │ └── listen/
│ │ │ ├── IServerListen.java
│ │ │ ├── ListenServerControl.java
│ │ │ ├── ServerListenThread.java
│ │ │ ├── clear/
│ │ │ │ ├── ClearInvalidSocketPartThread.java
│ │ │ │ └── IClearInvalidSocketPartThread.java
│ │ │ ├── config/
│ │ │ │ ├── AllSecretSimpleListenServerConfig.java
│ │ │ │ ├── IListenServerConfig.java
│ │ │ │ ├── MultControlListenServerConfig.java
│ │ │ │ ├── SecretSimpleListenServerConfig.java
│ │ │ │ └── SimpleListenServerConfig.java
│ │ │ ├── control/
│ │ │ │ ├── ControlSocket.java
│ │ │ │ ├── IControlSocket.java
│ │ │ │ └── MultiControlSocket.java
│ │ │ ├── recv/
│ │ │ │ ├── ClientHeartHandler.java
│ │ │ │ ├── CommonReplyHandler.java
│ │ │ │ └── IRecvHandler.java
│ │ │ └── serversocket/
│ │ │ └── ICreateServerSocket.java
│ │ └── utils/
│ │ ├── AESUtil.java
│ │ ├── Assert.java
│ │ ├── CountWaitLatch.java
│ │ ├── MD5Signature.java
│ │ └── Tools.java
│ └── resources/
│ └── logback.xml
└── test/
└── java/
└── person/
└── pluto/
└── TestMain.java
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
.*
!/.gitignore
target/
*.iml
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2020] [plutoppppp]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# natcross2
内网穿透工具
*********************
**Q群:438793541**
**群密码:natcross2**
## natcross是做什么的?
- 需要自己提供硬件支持、部署的内网穿透工具
- 提供TCP协议类型的内网穿透服务,包括但不限于http(s)、数据库连接、ssh等协议
- 支持https协议转http协议与应用交互方式
- 支持无加密、控制端口加密交互、数据加密交互方式
- 主要服务场景,需要将内网的应用开放到公网,如微信小程序开发调试、支付回调等
- 支持HTTP根据host进行反向代理;目标依然是内网应用,只是可以根据HTTP协议header中的host字段区分选择目标应用(注意:只是有人提出来了HTTP监听统一端口并用域名访问的问题,并且有做的价值才补充的该功能;没做负载功能,这个是内网穿透,不是nginx,更不建议直接用在生产上,需要负载的可以自己去实现)
## 打包使用
服务端-ServerApp打包:
```shell
# 修改ServerApp.java中serviceIp为公网服务器的IP
mvn clean compile package -PserverApp
```
客户端-ClientApp打包:
```shell
mvn clean compile package -PclientApp
```
## 安装
1. 需要一台可在公网访问的机器,并将控制端口(默认 servicePort 10010)、数据端口(如tomcat,开放公网 listenPort 8081端口,外网连接时要对应数据端口8081)开放出来
2. 将ServerApp.jar包放在公网可访问的机器并运行,打包前需要修改serviceIp为公网服务器的IP
3. 将clientApp为入口进行运行,destIp:destPort为要开放的内网应用,如本地的tomcat(127.0.0.1:8080)
4. 用浏览器访问serviceIp:listenPort便可以访问到内网的tomcat
## 参数解释
CommonConstants:
|字段|解释|
|:-:|:-|
|serviceIp|公网服务器的IP|
|servicePort|服务端的控制端口,主要用来与客户端进行指令交互|
|listenPort|服务端的监听端口,也就是部署完成后,在外网访问 serviceIp:listenPort 的方式对内网应用进行访问|
|destIp|要开放到公网的内网应用所在机器的IP|
|destPort|要开放到公网的内网应用的端口|
|aesKey|交互密钥key,保证数据的秘密性,可以查看 SecretInteractiveModel.java 中的fullMessage和checkAutograph中确认密钥的使用方式。
如果你使用了secretAll方式进行部署,这个key还是数据加密的key,可以在 SecretPassway.java 中确认密钥的使用方式
注意使用长度,windows版本的java只能用最大128长度的密钥|
|tokenKey|交互签名key,签名同aesKey|
ServerApp:
|字段|解释|
|:-:|:-|
|sslKeyStorePath|ssl证书的路径,默认方法只支持pkcs12的证书格式,使用这个证书可以做到https协议转http协议|
|sslKeyStorePassword|证书密码|
|createServerSocket|创建socket的方式,主要针对普通socket和sslSocket的方式进行封装,结合ssl证书使用|
ClientApp:
|字段|解释|
|:-:|:-|
|#secretHttpRoute:routes|http方式,根据不同host路由选择不同的目标应用|
## 内网穿透思路
因NAT网内CLIENT可以正常连接到SERVER端,并且能够保持一段时间的长连接,则由CLIENT发起连接,建立SOCKET对,在SERVER收到外部请求时,可以通过已经建立好的SOCKET将数据传输给CLIENT,CLIENT使用相同的方式将数据发送给指定的网络程序,网络程序回发数据后则按原路返回给请求方。

## 相关技术
|技术|体现点|
|:-:|:-|
|Socket|核心技术概念|
|NIO|nio.NioHallows,使用Selector作为注册监听器(多路复用),有事件唤起后会创建子线程进行异步处理|
|TCP粘包、拆包的解决|channel.LengthChannel,此处用的是一个大端序列的长度加消息内容的方式|
|线程管控|clientside.ClientControlThread、serverside.client.ClientServiceThread、serverside.listen.ServerListenThread作为独立管控及子线程异步处理的主要体现,亦可通过executor包下相关类进行追踪|
|HTTP路由|api.socketpart.HttpRouteSocketPart#routeHost,一个简单的对host头部字段的处理应用|
|消息加密|对AES、MD5联合的使用示例,channel.SecretInteractiveChannel、model.SecretInteractiveModel,对实际消息进行加密,增加辅助字段保证消息的真实性、准确性和完整性|
|计数门闩|utils.CountWaitLatch,类CountDownLatch,增加了countUp,不只受初始化的值决定,可以增加、减少,主要用来解决nio.NioHallows唤醒后批量channel注册的问题(等下,怎么感觉可以用读写锁来解决呢?)|
|线程池|虽然用了线程池,但默认的是Executors.newCachedThreadPool()来生成的,具体的还是需要根据机器来自定义线程池,主要还是对子线程的管控体现吧|
================================================
FILE: VERSION.md
================================================
# VERSION 2.3
1. 支持多客户端连接同一监听端口,从服务端进行负载分发(场景特殊,主要是面对客户端处在非稳定内网,使用多客户端进行多活保证)
# VERSION 2.2
1. socket使用channel方式,交互使用DMA策略
2. 监听端口、控制端口获得新连接后使用子线程方式进行异步操作
3. 优化了http路由算法,提高了路由选择时的速度
4. passWay使用了nio策略,统一交付NioHallows处理
5. ServerListenThread(当前长连接存在问题,故而目前使用bio形式获取)、ClientServiceThread使用了nio策略,统一交付NioHallows处理
6. 增加了CountWaitLatch(对标CountDownLatch,但增加的countUp,不再依赖初始化数量),用于NioHallows的等待注册过程
7. 修改pom.xml,去除无用的包,改为dependencyManagement方式进行版本管理,避免和引入项目发生版本冲突
# VERSION 2.1
支持http路由了,可以像nginx那样监听同一个端口,根据访问域名路由目标应用
注意http标准,需要头部有Host,并且注意换行严格要符合http的标准,且如果是长连接路由目标只会在第一次连接时选择目标
虽然上面有很多要求,但是一般使用标准的浏览器都不会出现问题的
================================================
FILE: pom.xml
================================================
* 客户端,放在内网侧 *
* * @author Pluto * @since 2020-01-09 16:26:44 */ public class ClientApp { public static void main(String[] args) throws Exception { // simple(); secret(); // secretAll(); // secretHttpRoute(); } /** * http路由 ** 默认使用交互加密、数据不加密的策略 */ public static void secretHttpRoute() throws Exception { HttpRoute[] routes = new HttpRoute[]{ // HttpRoute.of("localhost", "127.0.0.1", 8080), // HttpRoute.of(true, "127.0.0.1", "127.0.0.1", 8080), // }; for (ListenDest model : CommonConstants.listenDestArray) { SecretInteractiveClientConfig baseConfig = new SecretInteractiveClientConfig(); // 设置服务端IP和端口 baseConfig.setClientServiceIp(CommonConstants.serviceIp); baseConfig.setClientServicePort(CommonConstants.servicePort); // 设置对外暴露的端口,该端口的启动在服务端,这里只是表明要跟服务端的那个监听服务对接 baseConfig.setListenServerPort(model.listenPort); // 设置交互密钥和签名key baseConfig.setBaseAesKey(CommonConstants.aesKey); baseConfig.setTokenKey(CommonConstants.tokenKey); HttpRouteClientConfig config = new HttpRouteClientConfig(baseConfig); config.addRoute(routes); new ClientControlThread(config).createControl(); } } /** * 交互、隧道都进行加密 */ public static void secretAll() throws Exception { for (ListenDest model : CommonConstants.listenDestArray) { AllSecretInteractiveClientConfig config = new AllSecretInteractiveClientConfig(); // 设置服务端IP和端口 config.setClientServiceIp(CommonConstants.serviceIp); config.setClientServicePort(CommonConstants.servicePort); // 设置对外暴露的端口,该端口的启动在服务端,这里只是表明要跟服务端的那个监听服务对接 config.setListenServerPort(model.listenPort); // 设置要暴露的目标IP和端口 config.setDestIp(model.destIp); config.setDestPort(model.destPort); // 设置交互密钥和签名key config.setBaseAesKey(CommonConstants.aesKey); config.setTokenKey(CommonConstants.tokenKey); // 设置隧道交互密钥 config.setBasePasswayKey(CommonConstants.aesKey); new ClientControlThread(config).createControl(); } } /** * 交互加密,即交互验证 */ public static void secret() throws Exception { for (ListenDest model : CommonConstants.listenDestArray) { SecretInteractiveClientConfig config = new SecretInteractiveClientConfig(); // 设置服务端IP和端口 config.setClientServiceIp(CommonConstants.serviceIp); config.setClientServicePort(CommonConstants.servicePort); // 设置对外暴露的端口,该端口的启动在服务端,这里只是表明要跟服务端的那个监听服务对接 config.setListenServerPort(model.listenPort); // 设置要暴露的目标IP和端口 config.setDestIp(model.destIp); config.setDestPort(model.destPort); // 设置交互密钥和签名key config.setBaseAesKey(CommonConstants.aesKey); config.setTokenKey(CommonConstants.tokenKey); new ClientControlThread(config).createControl(); } } /** * 无加密、无验证 */ public static void simple() throws Exception { for (ListenDest model : CommonConstants.listenDestArray) { InteractiveClientConfig config = new InteractiveClientConfig(); // 设置服务端IP和端口 config.setClientServiceIp(CommonConstants.serviceIp); config.setClientServicePort(CommonConstants.servicePort); // 设置对外暴露的端口,该端口的启动在服务端,这里只是表明要跟服务端的那个监听服务对接 config.setListenServerPort(model.listenPort); // 设置要暴露的目标IP和端口 config.setDestIp(model.destIp); config.setDestPort(model.destPort); new ClientControlThread(config).createControl(); } } } ================================================ FILE: src/main/java/person/pluto/natcross2/CommonConstants.java ================================================ package person.pluto.natcross2; import lombok.AccessLevel; import lombok.NoArgsConstructor; /** *
* 公共参数 *
* * @author Pluto * @since 2020-04-10 12:29:01 */ @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class CommonConstants { // 服务端地址,支持IP或域名,这个根据服务端放的网络位置进行设置 public static final String serviceIp = "127.0.0.1"; // 客户端服务的端口 public static final int servicePort = 10010; // 映射对 public static ListenDest[] listenDestArray = new ListenDest[]{ // ListenDest.of(8081, "127.0.0.1", 8080), // }; // 交互密钥 AES public static final String aesKey = "8AUWlb+IWD+Fhbs0xnXCCg=="; // 交互签名key public static final String tokenKey = "tokenKey"; /** ** 监听、映射对 *
*/ static class ListenDest { public static ListenDest of(int listenPort, String destIp, int destPort) { ListenDest model = new ListenDest(); model.listenPort = listenPort; model.destIp = destIp; model.destPort = destPort; return model; } // 服务端监听的端口,外网访问服务端IP:listengPort即可进行穿透 public int listenPort; // 穿透的目标,即要暴露在外网的内网IP public String destIp; // 要暴露的内网端口 public int destPort; } } ================================================ FILE: src/main/java/person/pluto/natcross2/ServerApp.java ================================================ package person.pluto.natcross2; import org.apache.commons.lang3.StringUtils; import person.pluto.natcross2.CommonConstants.ListenDest; import person.pluto.natcross2.serverside.client.ClientServiceThread; import person.pluto.natcross2.serverside.client.config.SecretSimpleClientServiceConfig; import person.pluto.natcross2.serverside.client.config.SimpleClientServiceConfig; import person.pluto.natcross2.serverside.listen.ListenServerControl; import person.pluto.natcross2.serverside.listen.config.AllSecretSimpleListenServerConfig; import person.pluto.natcross2.serverside.listen.config.MultControlListenServerConfig; import person.pluto.natcross2.serverside.listen.config.SecretSimpleListenServerConfig; import person.pluto.natcross2.serverside.listen.config.SimpleListenServerConfig; import person.pluto.natcross2.serverside.listen.serversocket.ICreateServerSocket; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLServerSocketFactory; import java.nio.file.Files; import java.nio.file.Paths; import java.security.KeyStore; /** ** 服务端,放在外网侧 *
* * @author Pluto * @since 2020-01-09 16:27:03 */ public class ServerApp { // 你的p12格式的证书路径 private static final String sslKeyStorePath = System.getenv("sslKeyStorePath"); // 你的证书密码 private static final String sslKeyStorePassword = System.getenv("sslKeyStorePassword"); public static ICreateServerSocket createServerSocket; public static void main(String[] args) throws Exception { // 如果需要HTTPS协议的支持,则填写sslKeyStorePath、sslKeyStorePassword或在环境变量中定义 if (StringUtils.isNoneBlank(sslKeyStorePath, sslKeyStorePassword)) { createServerSocket = listenPort -> { KeyStore keystore = KeyStore.getInstance("PKCS12"); keystore.load(Files.newInputStream(Paths.get(sslKeyStorePath)), sslKeyStorePassword.toCharArray()); KeyManagerFactory keyFactory = KeyManagerFactory.getInstance("sunx509"); keyFactory.init(keystore, sslKeyStorePassword.toCharArray()); SSLContext ctx = SSLContext.getInstance("TLSv1.2"); ctx.init(keyFactory.getKeyManagers(), null, null); SSLServerSocketFactory serverSocketFactory = ctx.getServerSocketFactory(); return serverSocketFactory.createServerSocket(listenPort); }; } // simple(); secret(); // secretAll(); // multControlSecret(); } /** * 多客户端,控制通道加密 */ public static void multControlSecret() throws Exception { // 设置并启动客户端服务线程 SecretSimpleClientServiceConfig config = new SecretSimpleClientServiceConfig(CommonConstants.servicePort); // 设置交互aes密钥和签名密钥 config.setBaseAesKey(CommonConstants.aesKey); config.setTokenKey(CommonConstants.tokenKey); new ClientServiceThread(config).start(); for (ListenDest model : CommonConstants.listenDestArray) { // 设置并启动一个穿透端口 SecretSimpleListenServerConfig baseListengConfig = new SecretSimpleListenServerConfig(model.listenPort); // 设置交互aes密钥和签名密钥,这里使用和客户端服务相同的密钥,可以根据需要设置不同的 baseListengConfig.setBaseAesKey(CommonConstants.aesKey); baseListengConfig.setTokenKey(CommonConstants.tokenKey); baseListengConfig.setCreateServerSocket(createServerSocket); MultControlListenServerConfig listengConfig = new MultControlListenServerConfig(baseListengConfig); ListenServerControl.createNewListenServer(listengConfig); } } /** * 交互、隧道都进行加密 */ public static void secretAll() throws Exception { // 设置并启动客户端服务线程 SecretSimpleClientServiceConfig config = new SecretSimpleClientServiceConfig(CommonConstants.servicePort); // 设置交互aes密钥和签名密钥 config.setBaseAesKey(CommonConstants.aesKey); config.setTokenKey(CommonConstants.tokenKey); new ClientServiceThread(config).start(); for (ListenDest model : CommonConstants.listenDestArray) { AllSecretSimpleListenServerConfig listengConfig = new AllSecretSimpleListenServerConfig(model.listenPort); // 设置交互aes密钥和签名密钥,这里使用和客户端服务相同的密钥,可以根据需要设置不同的 listengConfig.setBaseAesKey(CommonConstants.aesKey); listengConfig.setTokenKey(CommonConstants.tokenKey); // 设置隧道密钥 listengConfig.setBasePasswayKey(CommonConstants.aesKey); listengConfig.setCreateServerSocket(createServerSocket); ListenServerControl.createNewListenServer(listengConfig); } } /** * 交互加密,即交互验证 */ public static void secret() throws Exception { // 设置并启动客户端服务线程 SecretSimpleClientServiceConfig config = new SecretSimpleClientServiceConfig(CommonConstants.servicePort); // 设置交互aes密钥和签名密钥 config.setBaseAesKey(CommonConstants.aesKey); config.setTokenKey(CommonConstants.tokenKey); new ClientServiceThread(config).start(); for (ListenDest model : CommonConstants.listenDestArray) { // 设置并启动一个穿透端口 SecretSimpleListenServerConfig listengConfig = new SecretSimpleListenServerConfig(model.listenPort); // 设置交互aes密钥和签名密钥,这里使用和客户端服务相同的密钥,可以根据需要设置不同的 listengConfig.setBaseAesKey(CommonConstants.aesKey); listengConfig.setTokenKey(CommonConstants.tokenKey); listengConfig.setCreateServerSocket(createServerSocket); ListenServerControl.createNewListenServer(listengConfig); } } /** * 无加密、无验证 */ public static void simple() throws Exception { // 设置并启动客户端服务线程 SimpleClientServiceConfig config = new SimpleClientServiceConfig(CommonConstants.servicePort); new ClientServiceThread(config).start(); for (ListenDest model : CommonConstants.listenDestArray) { // 设置并启动一个穿透端口 SimpleListenServerConfig listengConfig = new SimpleListenServerConfig(model.listenPort); listengConfig.setCreateServerSocket(createServerSocket); ListenServerControl.createNewListenServer(listengConfig); } } } ================================================ FILE: src/main/java/person/pluto/natcross2/api/IBelongControl.java ================================================ package person.pluto.natcross2.api; /** ** 通知上次停止的统一类,为适应不同的类型进行不同的函数封装 *
* * @author Pluto * @since 2019-07-12 08:39:25 */ public interface IBelongControl { /** * 无标记通知 */ default void noticeStop() { // do nothing } /** * 有标记通知 * * @param socketPartKey * @return */ default boolean stopSocketPart(String socketPartKey) { return true; } } ================================================ FILE: src/main/java/person/pluto/natcross2/api/IHttpRouting.java ================================================ package person.pluto.natcross2.api; import person.pluto.natcross2.model.HttpRoute; /** ** http 路由器 *
* * @author Pluto * @since 2021-04-26 08:54:44 */ public interface IHttpRouting { /** * 获取有效路由 * * @param host * @return */ HttpRoute pickEffectiveRoute(String host); } ================================================ FILE: src/main/java/person/pluto/natcross2/api/passway/SecretPassway.java ================================================ package person.pluto.natcross2.api.passway; import lombok.Data; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.api.IBelongControl; import person.pluto.natcross2.api.secret.ISecret; import person.pluto.natcross2.channel.LengthChannel; import person.pluto.natcross2.executor.NatcrossExecutor; import person.pluto.natcross2.nio.NioHallows; import person.pluto.natcross2.utils.Tools; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.util.Objects; /** ** 加密型隧道,一侧加密,一侧原样输入、输出 *
* * @author Pluto * @since 2020-01-08 15:55:29 */ @Data @Slf4j public class SecretPassway implements Runnable { /** * 通道模式 */ public enum Mode { /** * 从无加密接受到加密输出 */ noToSecret, /** * 从加密接受到无加密输出 */ secretToNo } private boolean alive = false; private ISecret secret; private Mode mode; private IBelongControl belongControl; private int streamCacheSize = 8192; private Socket recvSocket; private Socket sendSocket; @Override public void run() { try { if (Mode.noToSecret.equals(this.mode)) { noToSecret(); } else { secretToNo(); } } catch (Exception e) { // } log.debug("one InputToOutputThread closed"); // 传输完成后退出 this.cancel(); } /** * 从加密侧输入,解密后输出到无加密侧 * * @throws Exception */ private void secretToNo() throws Exception { LengthChannel recvChannel = new LengthChannel(this.recvSocket); OutputStream outputStream = this.sendSocket.getOutputStream(); SocketChannel outputChannel = this.sendSocket.getChannel(); ISecret secret = this.secret; byte[] read; byte[] decrypt; while (this.alive) { read = recvChannel.read(); if (read == null) { break; } decrypt = secret.decrypt(read); if (Objects.isNull(outputChannel)) { outputStream.write(decrypt); outputStream.flush(); } else { Tools.channelWrite(outputChannel, ByteBuffer.wrap(decrypt)); } } } /** * 从无加密侧,经过加密后输出到加密侧 * * @throws Exception */ private void noToSecret() throws Exception { InputStream inputStream = this.recvSocket.getInputStream(); LengthChannel sendChannel = new LengthChannel(this.sendSocket); // 字段赋值局部变量,入栈 boolean alive = this.alive; ISecret secret = this.secret; int len; byte[] arrayTemp = new byte[this.streamCacheSize]; byte[] encrypt; while (alive && (len = inputStream.read(arrayTemp)) > 0) { encrypt = secret.encrypt(arrayTemp, 0, len); sendChannel.writeAndFlush(encrypt); } } /** * 判断是否有效 * * @return */ public boolean isValid() { return this.alive; } /** * 退出 * * @author Pluto * @since 2020-01-08 15:57:48 */ public void cancel() { if (!this.alive) { return; } this.alive = false; NioHallows.release(this.recvSocket.getChannel()); try { Socket sendSocket; if ((sendSocket = this.sendSocket) != null) { // TCP 挥手步骤,对方调用 shutdownOutput 后等价完成 socket.close sendSocket.shutdownOutput(); } } catch (IOException e) { // do nothing } IBelongControl belong; if ((belong = this.belongControl) != null) { this.belongControl = null; belong.noticeStop(); } } /** * 启动 */ public void start() { if (this.alive) { return; } this.alive = true; NatcrossExecutor.executePassway(this); } } ================================================ FILE: src/main/java/person/pluto/natcross2/api/passway/SimplePassway.java ================================================ package person.pluto.natcross2.api.passway; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.api.IBelongControl; import person.pluto.natcross2.executor.NatcrossExecutor; import person.pluto.natcross2.nio.INioProcessor; import person.pluto.natcross2.nio.NioHallows; import person.pluto.natcross2.utils.Tools; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; import java.util.Objects; /** ** 简单的隧道,无任何处理,只从输入侧原样输出到输出侧 *
* * @author Pluto * @since 2020-01-08 15:58:11 */ @Slf4j public class SimplePassway implements Runnable, INioProcessor { private boolean alive = false; /** * 所属对象,完成后通知 */ @Setter private IBelongControl belongControl; /** * 缓存大小 */ @Setter private int streamCacheSize = 8192; @Setter private Socket recvSocket; @Setter private Socket sendSocket; private OutputStream outputStream; private SocketChannel outputChannel; private OutputStream getOutputStream() throws IOException { OutputStream outputStream = this.outputStream; if (Objects.isNull(outputStream)) { outputStream = this.sendSocket.getOutputStream(); this.outputStream = outputStream; } return outputStream; } private SocketChannel getOutputChannel() { SocketChannel outputChannel = this.outputChannel; if (Objects.isNull(outputChannel)) { outputChannel = this.sendSocket.getChannel(); this.outputChannel = outputChannel; } return outputChannel; } /** * 向输出通道输出数据 ** 这里不只是为了DMA而去用DMA,而是这里有奇葩问题 *
* 如能采用了SocketChannel,而去用outputStream的时候,不管输入输出,都会有奇怪的问题,比如输出会莫名的阻塞住 *
* 整体就是如果能用nio的方法,但是用了bio形式都会各种什么 NullPointException、IllageSateException 等等错误 *
* * @param byteBuffer * @throws IOException */ private void write(ByteBuffer byteBuffer) throws IOException { SocketChannel outputChannel = this.getOutputChannel(); if (Objects.nonNull(outputChannel)) { Tools.channelWrite(outputChannel, byteBuffer); } else { OutputStream outputStream = this.getOutputStream(); outputStream.write(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); outputStream.flush(); } } @Override public void run() { try { InputStream inputStream = this.recvSocket.getInputStream(); int len; byte[] arrayTemp = new byte[this.streamCacheSize]; while (this.alive && (len = inputStream.read(arrayTemp)) > 0) { this.write(ByteBuffer.wrap(arrayTemp, 0, len)); } } catch (IOException e) { // do nothing } log.debug("one InputToOutputThread closed"); // 传输完成后退出 this.cancel(); } // ============== nio ================= @Setter(AccessLevel.NONE) @Getter(AccessLevel.NONE) private ByteBuffer byteBuffer; private ByteBuffer obtainByteBuffer() { ByteBuffer byteBuffer = this.byteBuffer; if (Objects.isNull(byteBuffer)) { if (Objects.isNull(this.getOutputChannel())) { byteBuffer = ByteBuffer.allocate(this.streamCacheSize); } else { // 输入输出可以使用channel,此处则使用DirectByteBuffer,这时候才真正体现出了DMA byteBuffer = ByteBuffer.allocateDirect(this.streamCacheSize); } this.byteBuffer = byteBuffer; } return byteBuffer; } @Override public void process(SelectionKey key) { if (this.alive && key.isValid()) { ByteBuffer buffer = this.obtainByteBuffer(); SocketChannel inputChannel = (SocketChannel) key.channel(); try { int len; do { buffer.clear(); len = inputChannel.read(buffer); if (len > 0) { buffer.flip(); if (buffer.hasRemaining()) { this.write(buffer); } } } while (len > 0); // 如果不是负数,则还没有断开连接,返回继续等待 if (len == 0) { return; } } catch (IOException e) { // } } log.debug("one InputToOutputThread closed"); this.cancel(); } /** * 判断是否有效 * * @return */ public boolean isValid() { return this.alive; } /** * 退出 */ public void cancel() { if (!this.alive) { return; } this.alive = false; NioHallows.release(this.recvSocket.getChannel()); try { Socket sendSocket = this.sendSocket; if (Objects.nonNull(sendSocket)) { // TCP 挥手步骤,对方调用 shutdownOutput 后等价完成 socket.close sendSocket.shutdownOutput(); } } catch (IOException e) { // do nothing } IBelongControl belong; if ((belong = this.belongControl) != null) { this.belongControl = null; belong.noticeStop(); } } /** * 启动 */ public void start() { if (this.alive) { return; } this.alive = true; SocketChannel recvChannel = this.recvSocket.getChannel(); if (Objects.isNull(recvChannel)) { NatcrossExecutor.executePassway(this); } else { try { NioHallows.register(recvChannel, SelectionKey.OP_READ, this); } catch (IOException e) { log.error("nio register failed", e); this.cancel(); } } } } ================================================ FILE: src/main/java/person/pluto/natcross2/api/secret/AESSecret.java ================================================ package person.pluto.natcross2.api.secret; import lombok.Data; import person.pluto.natcross2.utils.AESUtil; import java.security.Key; /** *
* AES加密方式 *
* * @author Pluto * @since 2020-01-08 16:01:40 */ @Data public class AESSecret implements ISecret { private Key aesKey; @Override public byte[] encrypt(byte[] content, int offset, int len) throws Exception { return AESUtil.encrypt(this.aesKey, content, offset, len); } @Override public byte[] decrypt(byte[] result) throws Exception { return AESUtil.decrypt(this.aesKey, result); } /** * 设置密钥 * * @param aesKey */ public void setBaseAesKey(String aesKey) { this.aesKey = AESUtil.createKeyByBase64(aesKey); } } ================================================ FILE: src/main/java/person/pluto/natcross2/api/secret/ISecret.java ================================================ package person.pluto.natcross2.api.secret; /** ** 加密方法 *
* * @author Pluto * @since 2020-01-08 16:01:28 */ public interface ISecret { /** * 加密数据 * * @param content * @param offset * @param len * @return * @throws Exception */ byte[] encrypt(byte[] content, int offset, int len) throws Exception; /** * 解密数据 * * @param result * @return * @throws Exception */ byte[] decrypt(byte[] result) throws Exception; } ================================================ FILE: src/main/java/person/pluto/natcross2/api/socketpart/AbsSocketPart.java ================================================ package person.pluto.natcross2.api.socketpart; import lombok.AccessLevel; import lombok.Data; import lombok.Getter; import lombok.Setter; import person.pluto.natcross2.api.IBelongControl; import java.net.Socket; import java.time.Duration; import java.time.LocalDateTime; /** ** socketPart抽象类 *
* * @author Pluto * @since 2020-01-08 16:01:56 */ @Data public abstract class AbsSocketPart { @Setter(AccessLevel.PRIVATE) protected volatile boolean isAlive = false; @Setter(AccessLevel.PRIVATE) protected volatile boolean canceled = false; @Setter(AccessLevel.PRIVATE) protected LocalDateTime createTime; /** * 等待连接有效时间,ms */ @Getter @Setter private long invalidMillis = 60000L; @Setter(AccessLevel.NONE) @Getter(AccessLevel.PROTECTED) protected IBelongControl belongThread; protected String socketPartKey; /** * 接受数据的socket;接受与发送的区分主要是主动发送方 */ protected Socket recvSocket; /** * 发送的socket */ protected Socket sendSocket; protected AbsSocketPart(IBelongControl belongThread) { this.belongThread = belongThread; this.createTime = LocalDateTime.now(); } /** * 是否有效 */ public boolean isValid() { if (this.canceled) { return false; } if (this.isAlive) { return true; } long millis = Duration.between(this.createTime, LocalDateTime.now()).toMillis(); return millis < this.invalidMillis; } /** * 退出 */ public abstract void cancel(); /** * 打通隧道 * * @return */ public abstract boolean createPassWay(); } ================================================ FILE: src/main/java/person/pluto/natcross2/api/socketpart/HttpRouteSocketPart.java ================================================ package person.pluto.natcross2.api.socketpart; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.api.IBelongControl; import person.pluto.natcross2.api.IHttpRouting; import person.pluto.natcross2.api.passway.SimplePassway; import person.pluto.natcross2.model.HttpRoute; import person.pluto.natcross2.utils.Assert; import person.pluto.natcross2.utils.Tools; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; /** ** http路由socket对 *
* * @author Pluto * @since 2020-04-23 16:57:28 */ @Slf4j public class HttpRouteSocketPart extends SimpleSocketPart { private static final Charset httpCharset = StandardCharsets.ISO_8859_1; // 这里的 : 好巧不巧的 0x20 位是1,可以利用一波 private static final byte colonByte = ':'; private static final byte[] hostMatcher = new byte[]{'h', 'o', 's', 't', colonByte}; private static final int colonIndex = hostMatcher.length - 1; private final IHttpRouting httpRouting; public HttpRouteSocketPart(IBelongControl belongThread, IHttpRouting httpRouting) { super(belongThread); this.httpRouting = httpRouting; } /** * 选择路由并连接至目标 * * @throws Exception * @author Pluto * @since 2020-04-24 11:01:24 */ protected void routeHost() throws Exception { String host = null; BufferedInputStream inputStream = new BufferedInputStream(this.sendSocket.getInputStream()); // 缓存数据,不能我们处理了就不给实际应用 ByteArrayOutputStream headerBufferStream = new ByteArrayOutputStream(1024); // 临时输出列,用于读取一整行后进行字符串判断 ByteArrayOutputStream lineBufferStream = new ByteArrayOutputStream(); for (int flag = 0, lineCount = 0, matchFlag = 0; ; lineCount++) { // 依次读取 int read = inputStream.read(); lineBufferStream.write(read); if (read < 0) { break; } // 记录换行状态 if (read == '\r' || read == '\n') { flag++; } else { flag = 0; if ( // 这里matchFlag与lineCount不相等的频次比例较大,先比较 matchFlag == lineCount // 肯定要小于了呀 && matchFlag < hostMatcher.length // 大写转小写,如果是冒号的位置,需要完全相等 && hostMatcher[matchFlag] == (read | 0x20) && (matchFlag != colonIndex || colonByte == read) // ) { matchFlag++; } } // 如果大于等于4则就表示http头结束了 if (flag >= 4) { break; } // 等于2表示一行结束了,需要进行处理 if (flag == 2) { boolean isHostLine = (matchFlag == hostMatcher.length); // for循环特性,设置-1,营造line为0 lineCount = -1; matchFlag = 0; // 省去一次toByteArray拷贝的可能 lineBufferStream.writeTo(headerBufferStream); if (isHostLine) { byte[] byteArray = lineBufferStream.toByteArray(); // 重置行输出流 lineBufferStream.reset(); int left, right; byte rightByte; for (left = right = hostMatcher.length; right < byteArray.length; right++) { if (byteArray[left] == ' ') { // 左边先去掉空白,去除期间right不用判断 left++; } else if ( // (rightByte = byteArray[right]) == colonByte // || rightByte == ' ' || rightByte == '\r' || rightByte == '\n') { // right位置到left位置必有字符,遇到空白或 : 则停下,与left中间的组合为host地址 break; } } // 将缓存中的数据进行字符串化,根据http标准,字符集为 ISO-8859-1 host = new String(byteArray, left, right - left, httpCharset); break; } else { // 重置临时输出流 lineBufferStream.reset(); } } } // 将最后残留的输出 lineBufferStream.writeTo(headerBufferStream); Socket recvSocket = this.recvSocket; HttpRoute willConnect = this.httpRouting.pickEffectiveRoute(host); InetSocketAddress destAddress = new InetSocketAddress(willConnect.getDestIp(), willConnect.getDestPort()); recvSocket.connect(destAddress); OutputStream outputStream = recvSocket.getOutputStream(); headerBufferStream.writeTo(outputStream); // emmm.... 用bufferedStream每次read不用单字节从硬件缓存里读呀,快了些呢,咋地了,不就是再拷贝一次嘛! Tools.streamCopy(inputStream, outputStream); // flush的原因,不排除这里全部读完了,导致缓存中没有数据,那即使创建了passway也不会主动flush而是挂在那里,防止遇到lazy的自动刷新特性 outputStream.flush(); } @Override public boolean createPassWay() { Assert.state(!this.canceled, "不得重启已退出的socketPart"); if (this.isAlive) { return true; } this.isAlive = true; try { this.routeHost(); SimplePassway outToInPassway = this.outToInPassway = new SimplePassway(); outToInPassway.setBelongControl(this); outToInPassway.setSendSocket(this.sendSocket); outToInPassway.setRecvSocket(this.recvSocket); outToInPassway.setStreamCacheSize(this.getStreamCacheSize()); SimplePassway inToOutPassway = this.inToOutPassway = new SimplePassway(); inToOutPassway.setBelongControl(this); inToOutPassway.setSendSocket(this.recvSocket); inToOutPassway.setRecvSocket(this.sendSocket); inToOutPassway.setStreamCacheSize(this.getStreamCacheSize()); outToInPassway.start(); inToOutPassway.start(); } catch (Exception e) { log.error("socketPart [" + this.socketPartKey + "] 隧道建立异常", e); this.stop(); return false; } return true; } } ================================================ FILE: src/main/java/person/pluto/natcross2/api/socketpart/SecretSocketPart.java ================================================ package person.pluto.natcross2.api.socketpart; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.api.IBelongControl; import person.pluto.natcross2.api.passway.SecretPassway; import person.pluto.natcross2.api.passway.SecretPassway.Mode; import person.pluto.natcross2.api.secret.ISecret; import person.pluto.natcross2.utils.Assert; import java.io.IOException; import java.net.Socket; import java.util.concurrent.CountDownLatch; /** ** 加密-无加密socket对 *
* * @author Pluto * @since 2020-01-08 16:05:25 */ @Slf4j public class SecretSocketPart extends AbsSocketPart implements IBelongControl { @Getter @Setter private ISecret secret; private SecretPassway noToSecretPassway; private SecretPassway secretToNoPassway; private final CountDownLatch cancelLatch = new CountDownLatch(2); @Getter @Setter private int streamCacheSize = 8192; public SecretSocketPart(IBelongControl belongThread) { super(belongThread); } @Override public void cancel() { if (this.canceled) { return; } this.canceled = true; this.isAlive = false; log.debug("socketPart {} will cancel", this.socketPartKey); SecretPassway noToSecretPassway; if ((noToSecretPassway = this.noToSecretPassway) != null) { this.noToSecretPassway = null; noToSecretPassway.cancel(); } SecretPassway secretToNoPassway; if ((secretToNoPassway = this.secretToNoPassway) != null) { this.secretToNoPassway = null; secretToNoPassway.cancel(); } Socket recvSocket; if ((recvSocket = this.recvSocket) != null) { this.recvSocket = null; try { recvSocket.close(); } catch (IOException e) { log.debug("socketPart [{}] 监听端口 关闭异常", socketPartKey); } } Socket sendSocket; if ((sendSocket = this.sendSocket) != null) { this.sendSocket = null; try { sendSocket.close(); } catch (IOException e) { log.debug("socketPart [{}] 发送端口 关闭异常", socketPartKey); } } log.debug("socketPart {} is cancelled", this.socketPartKey); } @Override public boolean createPassWay() { Assert.state(!this.canceled, "不得重启已退出的socketPart"); if (this.isAlive) { return true; } this.isAlive = true; try { // 主要面向服务端-客户端过程加密 SecretPassway noToSecretPassway = this.noToSecretPassway = new SecretPassway(); noToSecretPassway.setBelongControl(this); noToSecretPassway.setMode(Mode.noToSecret); noToSecretPassway.setRecvSocket(this.recvSocket); noToSecretPassway.setSendSocket(this.sendSocket); noToSecretPassway.setStreamCacheSize(getStreamCacheSize()); noToSecretPassway.setSecret(this.secret); SecretPassway secretToNoPassway = this.secretToNoPassway = new SecretPassway(); secretToNoPassway.setBelongControl(this); secretToNoPassway.setMode(Mode.secretToNo); secretToNoPassway.setRecvSocket(this.sendSocket); secretToNoPassway.setSendSocket(this.recvSocket); secretToNoPassway.setSecret(this.secret); noToSecretPassway.start(); secretToNoPassway.start(); } catch (Exception e) { log.error("socketPart [" + this.socketPartKey + "] 隧道建立异常", e); this.stop(); return false; } return true; } /** * 停止 */ public void stop() { this.cancel(); IBelongControl belong; if ((belong = this.belongThread) != null) { this.belongThread = null; belong.noticeStop(); } } @Override public void noticeStop() { CountDownLatch cancelLatch = this.cancelLatch; cancelLatch.countDown(); if (cancelLatch.getCount() <= 0) { this.stop(); } } } ================================================ FILE: src/main/java/person/pluto/natcross2/api/socketpart/SimpleSocketPart.java ================================================ package person.pluto.natcross2.api.socketpart; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.api.IBelongControl; import person.pluto.natcross2.api.passway.SimplePassway; import person.pluto.natcross2.utils.Assert; import java.io.IOException; import java.net.Socket; import java.util.concurrent.CountDownLatch; /** ** socket匹配对 *
* * @author Pluto * @since 2019-07-12 08:36:30 */ @Slf4j public class SimpleSocketPart extends AbsSocketPart implements IBelongControl { protected SimplePassway outToInPassway; protected SimplePassway inToOutPassway; private final CountDownLatch cancelLatch = new CountDownLatch(2); @Getter @Setter private int streamCacheSize = 8192; public SimpleSocketPart(IBelongControl belongThread) { super(belongThread); } /** * 停止,并告知上层处理掉 */ public void stop() { this.cancel(); IBelongControl belong; if ((belong = this.belongThread) != null) { this.belongThread = null; belong.stopSocketPart(this.socketPartKey); } } @Override public void cancel() { if (this.canceled) { return; } this.canceled = true; this.isAlive = false; log.debug("socketPart {} will cancel", this.socketPartKey); SimplePassway outToInPassway; if ((outToInPassway = this.outToInPassway) != null) { this.outToInPassway = null; outToInPassway.cancel(); } SimplePassway inToOutPassway; if ((inToOutPassway = this.inToOutPassway) != null) { this.inToOutPassway = null; inToOutPassway.cancel(); } Socket recvSocket; if ((recvSocket = this.recvSocket) != null) { this.recvSocket = null; try { recvSocket.close(); } catch (IOException e) { log.debug("socketPart [{}] 监听端口 关闭异常", socketPartKey); } } Socket sendSocket; if ((sendSocket = this.sendSocket) != null) { this.sendSocket = null; try { sendSocket.close(); } catch (IOException e) { log.debug("socketPart [{}] 发送端口 关闭异常", socketPartKey); } } log.debug("socketPart {} is cancelled", this.socketPartKey); } @Override public boolean createPassWay() { Assert.state(!this.canceled, "不得重启已退出的socketPart"); if (this.isAlive) { return true; } this.isAlive = true; try { SimplePassway outToInPassway = this.outToInPassway = new SimplePassway(); outToInPassway.setBelongControl(this); outToInPassway.setRecvSocket(this.recvSocket); outToInPassway.setSendSocket(this.sendSocket); outToInPassway.setStreamCacheSize(getStreamCacheSize()); SimplePassway inToOutPassway = this.inToOutPassway = new SimplePassway(); inToOutPassway.setBelongControl(this); inToOutPassway.setRecvSocket(this.sendSocket); inToOutPassway.setSendSocket(this.recvSocket); inToOutPassway.setStreamCacheSize(getStreamCacheSize()); outToInPassway.start(); inToOutPassway.start(); } catch (Exception e) { log.error("socketPart [" + this.socketPartKey + "] 隧道建立异常", e); this.stop(); return false; } return true; } @Override public void noticeStop() { CountDownLatch cancellLatch = this.cancelLatch; cancellLatch.countDown(); if (cancellLatch.getCount() <= 0) { this.stop(); } } } ================================================ FILE: src/main/java/person/pluto/natcross2/channel/Channel.java ================================================ package person.pluto.natcross2.channel; import java.io.Closeable; import java.nio.charset.Charset; /** ** 读写通道 *
* * @param* InteractiveModel 模式读写 *
* * @author Pluto * @since 2020-01-08 16:11:50 */ public class InteractiveChannel extends SocketChannel* json方式读写 *
* * @author Pluto * @since 2020-01-08 16:13:02 */ public class JsonChannel extends SocketChannel* 长度限定读写通道 *
* * @author Pluto * @since 2021-04-08 12:42:38 */ public class LengthChannel extends SocketChannel* InteractiveModel 加密型通道,AES加密 *
* * @author Pluto * @since 2020-01-08 16:16:12 */ @Data @EqualsAndHashCode(callSuper = false) public class SecretInteractiveChannel extends SocketChannel* socket通道 *
* * @param* 字符型通道 *
* * @author Pluto * @since 2020-01-08 16:21:31 */ public class StringChannel extends SocketChannel* 客户端控制服务 *
* * @author Pluto * @since 2019-07-05 10:53:33 */ @Slf4j public final class ClientControlThread implements Runnable, IBelongControl { private volatile Thread myThread = null; private volatile boolean isAlive = false; @Getter private volatile boolean cancelled = false; private final Map* 客户端适配器 *
* * @param* 取 {@link LocalDateTime#now()} 为 设定值 * * @return */ default LocalDateTime resetServerHeartLastRecvTime() { return this.resetServerHeartLastRecvTime(LocalDateTime.now()); } /** * 重设服务端最后心跳测试/回复时间 * * @param time 自有设置 * @return */ LocalDateTime resetServerHeartLastRecvTime(LocalDateTime time); /** * 获取socket读写通道 * * @return */ SocketChannel extends R, ? super W> getSocketChannel(); } ================================================ FILE: src/main/java/person/pluto/natcross2/clientside/adapter/InteractiveSimpleClientAdapter.java ================================================ package person.pluto.natcross2.clientside.adapter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import person.pluto.natcross2.api.socketpart.AbsSocketPart; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.clientside.ClientControlThread; import person.pluto.natcross2.clientside.config.IClientConfig; import person.pluto.natcross2.clientside.handler.IClientHandler; import person.pluto.natcross2.executor.NatcrossExecutor; import person.pluto.natcross2.model.InteractiveModel; import person.pluto.natcross2.model.NatcrossResultModel; import person.pluto.natcross2.model.enumeration.InteractiveTypeEnum; import person.pluto.natcross2.model.enumeration.NatcrossResultEnum; import person.pluto.natcross2.model.interactive.ClientControlModel; import person.pluto.natcross2.model.interactive.ServerWaitModel; import java.io.IOException; import java.net.Socket; import java.time.LocalDateTime; import java.util.LinkedList; import java.util.List; import java.util.Objects; /** *
* 基于InteractiveModel的客户端适配器 *
* * @author Pluto * @since 2020-01-08 16:24:07 */ @Slf4j public class InteractiveSimpleClientAdapter implements IClientAdapter* 交互及隧道都加密 *
* * @author Pluto * @since 2020-01-08 15:01:44 */ public class AllSecretInteractiveClientConfig extends SecretInteractiveClientConfig { @Setter @Getter private Key passwayKey; @Override public AbsSocketPart newSocketPart(ClientControlThread clientControlThread) { AESSecret secret = new AESSecret(); secret.setAesKey(this.passwayKey); SecretSocketPart secretSocketPart = new SecretSocketPart(clientControlThread); secretSocketPart.setSecret(secret); return secretSocketPart; } /** * base64格式设置密钥 * * @param key */ public void setBasePasswayKey(String key) { this.passwayKey = AESUtil.createKeyByBase64(key); } } ================================================ FILE: src/main/java/person/pluto/natcross2/clientside/config/HttpRouteClientConfig.java ================================================ package person.pluto.natcross2.clientside.config; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import person.pluto.natcross2.api.IHttpRouting; import person.pluto.natcross2.api.socketpart.AbsSocketPart; import person.pluto.natcross2.api.socketpart.HttpRouteSocketPart; import person.pluto.natcross2.channel.SocketChannel; import person.pluto.natcross2.clientside.ClientControlThread; import person.pluto.natcross2.clientside.adapter.IClientAdapter; import person.pluto.natcross2.clientside.adapter.InteractiveSimpleClientAdapter; import person.pluto.natcross2.clientside.handler.CommonReplyHandler; import person.pluto.natcross2.clientside.handler.ServerHeartHandler; import person.pluto.natcross2.clientside.handler.ServerWaitClientHandler; import person.pluto.natcross2.clientside.heart.IClientHeartThread; import person.pluto.natcross2.model.HttpRoute; import person.pluto.natcross2.model.InteractiveModel; import person.pluto.natcross2.utils.Assert; import java.net.Socket; import java.nio.channels.spi.SelectorProvider; import java.nio.charset.Charset; import java.util.*; import java.util.concurrent.locks.StampedLock; /** ** http路由客户端配置 *
* * @author Pluto * @since 2020-04-24 10:09:46 */ @Slf4j public class HttpRouteClientConfig extends InteractiveClientConfig implements IHttpRouting { private final InteractiveClientConfig baseConfig; @Getter private HttpRoute masterRoute = null; // 不可对routeMap进行修改,只得以重新赋值方式重设 private Map* 客户端配置接口 *
* * @param* 简单的以InteractiveModel为交互模型的配置 *
* * @author Pluto * @since 2020-01-08 16:30:53 */ @Slf4j @Data public class InteractiveClientConfig implements IClientConfig* 交互加密的配置方案(AES加密) *
* * @author Pluto * @since 2020-01-08 16:32:18 */ @Slf4j @Data @EqualsAndHashCode(callSuper = false) public class SecretInteractiveClientConfig extends InteractiveClientConfig { private String tokenKey; private Key aesKey; @Override public SocketChannel extends InteractiveModel, ? super InteractiveModel> newClientChannel() { SecretInteractiveChannel channel = new SecretInteractiveChannel(); channel.setCharset(this.getCharset()); channel.setTokenKey(this.tokenKey); channel.setAesKey(this.aesKey); try { Socket socket = new Socket(this.getClientServiceIp(), this.getClientServicePort()); channel.setSocket(socket); } catch (IOException e) { log.debug("connect client service exception", e); throw new RuntimeException(e); } return channel; } /** * 设置交互密钥 * * @param aesKey */ public void setBaseAesKey(String aesKey) { this.aesKey = AESUtil.createKeyByBase64(aesKey); } } ================================================ FILE: src/main/java/person/pluto/natcross2/clientside/handler/CommonReplyHandler.java ================================================ package person.pluto.natcross2.clientside.handler; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.clientside.adapter.IClientAdapter; import person.pluto.natcross2.model.InteractiveModel; import person.pluto.natcross2.model.enumeration.InteractiveTypeEnum; import java.util.Objects; /** ** 统一回复 处理器 *
* * @author Pluto * @since 2020-04-15 13:02:09 */ @Slf4j public class CommonReplyHandler implements IClientHandler* 接收处理器 *
* * @author Pluto * @since 2020-04-15 11:13:20 */ public interface IClientHandler* 心跳检测 *
* * @author Pluto * @since 2020-04-15 13:02:09 */ @Slf4j public class ServerHeartHandler implements IClientHandler* 心跳检测 *
* * @author Pluto * @since 2020-04-15 13:02:09 */ public class ServerWaitClientHandler implements IClientHandler* 心跳检测线程 *
* * @author Pluto * @since 2021-04-25 17:53:44 */ @Slf4j public class ClientHeartThread implements IClientHeartThread, Runnable { private final ClientControlThread clientControlThread; private volatile boolean isAlive = false; @Setter @Getter private long heartIntervalSeconds = 10L; @Setter @Getter private int tryRecipientCount = 10; @Setter @Getter private long serverHeartMaxMissDurationSeconds = 25L; private volatile ScheduledFuture> scheduledFuture; private int failCount = 0; public ClientHeartThread(ClientControlThread clientControlThread) { this.clientControlThread = clientControlThread; } /** * 检查服务端是否心跳超时 * * @param clientControlThread * @param timeoutSeconds * @return */ private boolean lastServerHeartEffective(ClientControlThread clientControlThread, Long timeoutSeconds) { return Duration.between(clientControlThread.obtainServerHeartLastRecvTime(), LocalDateTime.now()) .get(ChronoUnit.SECONDS) < timeoutSeconds; } @Override public void run() { ClientControlThread clientControlThread = this.clientControlThread; if (clientControlThread.isCancelled() || !this.isAlive()) { this.cancel(); } log.debug("send client heart data to {}", clientControlThread.getListenServerPort()); try { // 如果服务端心跳超时则直接判定为失败,否则进行心跳检查 if (this.lastServerHeartEffective(clientControlThread, this.serverHeartMaxMissDurationSeconds)) { clientControlThread.sendHeartTest(); this.failCount = 0; return; } } catch (Exception e) { log.warn("{} 心跳异常", clientControlThread.getListenServerPort()); clientControlThread.stopClient(); } if (!this.isAlive) { return; } this.failCount++; boolean createControl = false, logFlag = true; try { createControl = clientControlThread.createControl(); } catch (Exception reClientException) { log.warn("重新建立连接" + clientControlThread.getListenServerPort() + "失败第 " + this.failCount + " 次", reClientException); logFlag = false; } if (createControl) { log.info("重新建立连接 {} 成功,在第 {} 次", clientControlThread.getListenServerPort(), this.failCount); this.failCount = 0; return; } if (logFlag) { log.warn("重新建立连接" + clientControlThread.getListenServerPort() + "失败第 " + this.failCount + " 次"); } if (this.failCount >= this.tryRecipientCount) { log.error("尝试重新连接 {} 超过最大次数,关闭客户端", clientControlThread.getListenServerPort()); clientControlThread.cancell(); this.cancel(); } } @Override public synchronized void start() { this.isAlive = true; ScheduledFuture> scheduledFuture = this.scheduledFuture; if (Objects.isNull(scheduledFuture) || scheduledFuture.isCancelled()) { this.failCount = 0; this.scheduledFuture = NatcrossExecutor.scheduledClientHeart(this, this.heartIntervalSeconds); } } @Override public void cancel() { if (!this.isAlive) { return; } this.isAlive = false; ScheduledFuture> scheduledFuture = this.scheduledFuture; if (Objects.nonNull(scheduledFuture) && !scheduledFuture.isCancelled()) { this.scheduledFuture = null; scheduledFuture.cancel(false); } } @Override public boolean isAlive() { return this.isAlive; } } ================================================ FILE: src/main/java/person/pluto/natcross2/clientside/heart/IClientHeartThread.java ================================================ package person.pluto.natcross2.clientside.heart; /** ** 心跳测试线程 *
* * @author Pluto * @since 2020-01-08 16:33:03 */ public interface IClientHeartThread { /** * 是否还活着 * * @return */ boolean isAlive(); /** * 退出 */ void cancel(); /** * 开始 */ void start(); } ================================================ FILE: src/main/java/person/pluto/natcross2/common/CommonFormat.java ================================================ package person.pluto.natcross2.common; import lombok.AccessLevel; import lombok.NoArgsConstructor; import org.apache.commons.lang3.RandomStringUtils; import java.text.DecimalFormat; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; /** ** 公用的格式化类 *
* * @author Pluto * @since 2019-07-05 13:35:04 */ @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class CommonFormat { /** * 获取socket匹配对key * * @param listenPort * @return */ public static String generateSocketPartKey(Integer listenPort) { DecimalFormat fiveLenFormat = new DecimalFormat("00000"); String dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")); String randomNum = RandomStringUtils.randomNumeric(4); return String.join("-", "SK", fiveLenFormat.format(listenPort), dateTime, randomNum); } /** * 根据socketPartKey获取端口号 * * @param socketPartKey * @return */ public static Integer getSocketPortByPartKey(String socketPartKey) { String[] split = socketPartKey.split("-"); return Integer.valueOf(split[1]); } /** * 获取交互流水号 * * @return */ public static String generateInteractiveSeq() { String dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")); String randomNum = RandomStringUtils.randomNumeric(4); return String.join("-", "IS", dateTime, randomNum); } } ================================================ FILE: src/main/java/person/pluto/natcross2/common/Optional.java ================================================ package person.pluto.natcross2.common; /** ** 操作对象,主要是让值能够通过引用进行传递 *
* * @param* 执行器实现 *
* * @author Pluto * @since 2021-04-08 14:38:23 */ public interface IExecutor { /** * 关闭 * * @throws Exception */ void shutdown(); /** * 默认执行方法 * * @param runnable */ void execute(Runnable runnable); /** * 服务监听线程任务执行器 ** For {@link person.pluto.natcross2.serverside.listen.ServerListenThread} * * @param runnable */ default void executeServerListenAccept(Runnable runnable) { execute(runnable); } /** * 客户端监听线程任务执行器 *
* For {@link person.pluto.natcross2.serverside.client.ClientServiceThread} * * @param runnable */ default void executeClientServiceAccept(Runnable runnable) { execute(runnable); } /** * 客户端消息处理任务执行器 *
* For * {@link person.pluto.natcross2.clientside.adapter.InteractiveSimpleClientAdapter#waitMessage()} * * @param runnable */ default void executeClientMessageProc(Runnable runnable) { execute(runnable); } /** * 隧道线程执行器 *
* For {@link person.pluto.natcross2.api.passway} * * @param runnable */ default void executePassway(Runnable runnable) { execute(runnable); } /** * nio事件任务执行器 *
* For {@link person.pluto.natcross2.nio.NioHallows#run()} * * @param runnable */ default void executeNioAction(Runnable runnable) { execute(runnable); } /** * 心跳检测定时循环任务执行 * * @param runnable * @param delaySeconds */ ScheduledFuture> scheduledClientHeart(Runnable runnable, long delaySeconds); /** * 服务监听清理无效socket对 * * @param runnable * @param delaySeconds * @return */ ScheduledFuture> scheduledClearInvalidSocketPart(Runnable runnable, long delaySeconds); } ================================================ FILE: src/main/java/person/pluto/natcross2/executor/NatcrossExecutor.java ================================================ package person.pluto.natcross2.executor; import lombok.AccessLevel; import lombok.NoArgsConstructor; import java.util.Objects; import java.util.concurrent.ScheduledFuture; /** *
* 线程执行器 *
* 主要是为了统一位置,方便管理 *
* * @author Pluto * @since 2021-04-08 14:37:52 */ @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class NatcrossExecutor { private static volatile IExecutor INSTANCE = new SimpleExecutor(); public static void shutdown() { INSTANCE.shutdown(); } /** * 重设执行器 ** 会将旧的执行器进行执行 {@link IExecutor#shutdown()} 方法,建议重设执行器的操作在初始化程序时 * * @param executor */ public static void resetExecutor(IExecutor executor) { IExecutor oldExecutor = INSTANCE; if (Objects.nonNull(oldExecutor)) { try { oldExecutor.shutdown(); } catch (Exception e) { // } } INSTANCE = executor; } /** * 服务监听线程任务执行器 *
* For {@link person.pluto.natcross2.serverside.listen.ServerListenThread} * * @param runnable */ public static void executeServerListenAccept(Runnable runnable) { INSTANCE.executeServerListenAccept(runnable); } /** * 客户端监听线程任务执行器 *
* For {@link person.pluto.natcross2.serverside.client.ClientServiceThread} * * @param runnable */ public static void executeClientServiceAccept(Runnable runnable) { INSTANCE.executeClientServiceAccept(runnable); } /** * 客户端消息处理任务执行器 *
* For * {@link person.pluto.natcross2.clientside.adapter.InteractiveSimpleClientAdapter#waitMessage()} * * @param runnable */ public static void executeClientMessageProc(Runnable runnable) { INSTANCE.executeClientMessageProc(runnable); } /** * 隧道线程执行器 *
* For {@link person.pluto.natcross2.api.passway} * * @param runnable */ public static void executePassway(Runnable runnable) { INSTANCE.executePassway(runnable); } /** * nio事件任务执行器 *
* For {@link person.pluto.natcross2.nio.NioHallows#run()} * * @param runnable */ public static void executeNioAction(Runnable runnable) { INSTANCE.executeNioAction(runnable); } /** * 心跳检测定时循环任务执行 * * @param runnable * @param delaySeconds */ public static ScheduledFuture> scheduledClientHeart(Runnable runnable, long delaySeconds) { return INSTANCE.scheduledClientHeart(runnable, delaySeconds); } /** * 服务监听清理无效socket对 * * @param runnable * @param delaySeconds * @return */ public static ScheduledFuture> scheduledClearInvalidSocketPart(Runnable runnable, long delaySeconds) { return INSTANCE.scheduledClearInvalidSocketPart(runnable, delaySeconds); } } ================================================ FILE: src/main/java/person/pluto/natcross2/executor/SimpleExecutor.java ================================================ package person.pluto.natcross2.executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; /** *
* 线程执行器 *
* * @author Pluto * @since 2021-04-09 12:46:36 */ public class SimpleExecutor implements IExecutor { private final ExecutorService executor = Executors.newCachedThreadPool(); private final ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); @Override public void shutdown() { this.executor.shutdownNow(); this.scheduledExecutor.shutdownNow(); } @Override public void execute(Runnable runnable) { this.executor.execute(runnable); } @Override public ScheduledFuture> scheduledClientHeart(Runnable runnable, long delaySeconds) { return this.scheduledExecutor.scheduleWithFixedDelay(runnable, delaySeconds, delaySeconds, TimeUnit.SECONDS); } @Override public ScheduledFuture> scheduledClearInvalidSocketPart(Runnable runnable, long delaySeconds) { return this.scheduledExecutor.scheduleWithFixedDelay(runnable, delaySeconds, delaySeconds, TimeUnit.SECONDS); } } ================================================ FILE: src/main/java/person/pluto/natcross2/model/HttpRoute.java ================================================ package person.pluto.natcross2.model; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; /** ** http路由表 *
* * @author Pluto * @since 2020-04-24 09:31:51 */ @Getter @Setter(AccessLevel.PRIVATE) public class HttpRoute { public static HttpRoute of(String host, String destIp, Integer destPort) { return HttpRoute.of(false, host, destIp, destPort); } public static HttpRoute of(boolean master, String host, String destIp, Integer destPort) { HttpRoute model = new HttpRoute(); model.setMaster(master); model.setHost(host); model.setDestIp(destIp); model.setDestPort(destPort); return model; } // 主路由,如果是多个则会去队列最后设置的那个 private boolean master; // 请求时的域名host private String host; // 目标IP private String destIp; // 目标端口 private Integer destPort; } ================================================ FILE: src/main/java/person/pluto/natcross2/model/InteractiveModel.java ================================================ package person.pluto.natcross2.model; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONAware; import com.alibaba.fastjson.JSONObject; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; import person.pluto.natcross2.common.CommonFormat; import person.pluto.natcross2.model.enumeration.InteractiveTypeEnum; /** ** 交互基础类型 *
* * @author Pluto * @since 2019-07-18 18:16:34 */ @Data @Accessors(chain = true) @NoArgsConstructor @AllArgsConstructor public class InteractiveModel implements JSONAware { public static InteractiveModel of(InteractiveTypeEnum interactiveTypeEnum, String key, String value) { JSONObject jsonObject = new JSONObject(); jsonObject.put(key, value); return new InteractiveModel(CommonFormat.generateInteractiveSeq(), interactiveTypeEnum.name(), jsonObject); } public static InteractiveModel of(String interactiveSeq, InteractiveTypeEnum interactiveTypeEnum, Object data) { return new InteractiveModel(interactiveSeq, interactiveTypeEnum.name(), data == null ? null : JSON.parseObject(JSON.toJSONString(data))); } public static InteractiveModel of(InteractiveTypeEnum interactiveTypeEnum, Object data) { return new InteractiveModel(CommonFormat.generateInteractiveSeq(), interactiveTypeEnum.name(), data == null ? null : JSON.parseObject(JSON.toJSONString(data))); } public static InteractiveModel of(String interactiveType, Object data) { return new InteractiveModel(CommonFormat.generateInteractiveSeq(), interactiveType, JSON.parseObject(JSON.toJSONString(data))); } public InteractiveModel(InteractiveModel model) { this.fullValue(model); } public void fullValue(InteractiveModel model) { this.setInteractiveSeq(model.getInteractiveSeq()); this.setInteractiveType(model.getInteractiveType()); this.setData(model.getData()); } /** * 交互序列,用于异步通信 */ private String interactiveSeq; /** * 交互类型 */ private String interactiveType; /** * 交互实体内容 */ private JSONObject data; @Override public String toJSONString() { JSONObject jsonObject = new JSONObject(); jsonObject.put("interactiveSeq", this.interactiveSeq); jsonObject.put("interactiveType", this.interactiveType); jsonObject.put("data", this.data); return jsonObject.toJSONString(); } } ================================================ FILE: src/main/java/person/pluto/natcross2/model/NatcrossResultModel.java ================================================ package person.pluto.natcross2.model; import com.alibaba.fastjson.JSONAware; import com.alibaba.fastjson.JSONObject; import lombok.Data; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.model.enumeration.NatcrossResultEnum; import java.lang.reflect.Field; /** ** 常规类型的前后端返回model *
* * @author Pluto * @since 2019-03-28 10:45:41 */ @Data @Slf4j public class NatcrossResultModel implements JSONAware { public static NatcrossResultModel of(String retCode, String retMsg, Object object) { return new NatcrossResultModel(retCode, retMsg, object); } public static NatcrossResultModel of(NatcrossResultEnum resultEnum, Object data) { return new NatcrossResultModel(resultEnum.getCode(), resultEnum.getName(), data); } public static NatcrossResultModel of(NatcrossResultEnum resultEnum) { return new NatcrossResultModel(resultEnum.getCode(), resultEnum.getName(), null); } public static NatcrossResultModel ofFail(Object data) { return new NatcrossResultModel(NatcrossResultEnum.FAIL.getCode(), NatcrossResultEnum.FAIL.getName(), data); } public static NatcrossResultModel ofFail() { return new NatcrossResultModel(NatcrossResultEnum.FAIL.getCode(), NatcrossResultEnum.FAIL.getName(), null); } public static NatcrossResultModel ofSuccess(Object data) { return new NatcrossResultModel(NatcrossResultEnum.SUCCESS.getCode(), NatcrossResultEnum.SUCCESS.getName(), data); } public static NatcrossResultModel ofSuccess() { return new NatcrossResultModel(NatcrossResultEnum.SUCCESS.getCode(), NatcrossResultEnum.SUCCESS.getName(), null); } private String retCod; private String retMsg; private Object data; public NatcrossResultModel(String retCod, String retMsg, Object data) { this.retCod = retCod; this.retMsg = retMsg; this.data = data; } /** * 反射方式修改值 * * @param fieldStr * @param object * @return * @author Pluto * @since 2019-05-10 14:04:48 */ public NatcrossResultModel set(String fieldStr, Object object) { Field field; try { field = this.getClass().getDeclaredField(fieldStr); } catch (NoSuchFieldException | SecurityException e) { log.warn("ResultModel get field failed!", e); return this; } field.setAccessible(true); try { field.set(this, object); } catch (IllegalArgumentException | IllegalAccessException e) { log.warn("ResultModel set field failed!", e); return this; } return this; } @Override public String toString() { return this.toJSONString(); } @Override public String toJSONString() { JSONObject jsonObject = new JSONObject(); jsonObject.put("retCod", this.retCod); jsonObject.put("retMsg", this.retMsg); jsonObject.put("data", this.data); return jsonObject.toJSONString(); } } ================================================ FILE: src/main/java/person/pluto/natcross2/model/SecretInteractiveModel.java ================================================ package person.pluto.natcross2.model; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; import org.apache.commons.lang3.StringUtils; import person.pluto.natcross2.utils.AESUtil; import person.pluto.natcross2.utils.MD5Signature; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.Key; /** ** 基于InteractiveModel模型的加密交互模型 *
* * @author Pluto * @since 2020-01-08 16:38:52 */ @Data @NoArgsConstructor @ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) public class SecretInteractiveModel extends InteractiveModel { public SecretInteractiveModel(InteractiveModel model) { super(model); } /** * 时间戳 */ private Long timestamp; /** * 签名 */ private String autograph; /** * InteractiveModel模型jsonString加密值 */ private String encrypt; /** * 字符编码 */ private String charset = StandardCharsets.UTF_8.name(); /** * 加密消息 * * @param key * @throws Exception * @author Pluto * @since 2020-01-08 16:39:46 */ public void encryptMsg(Key key) throws Exception { this.encrypt = AESUtil.encryptBase64(key, super.toJSONString().getBytes(this.charset)); } /** * 解密消息 * * @param key * @throws Exception * @author Pluto * @since 2020-01-08 16:39:53 */ public void decryptMsg(Key key) throws Exception { byte[] decryptBase64 = AESUtil.decryptBase64(key, this.encrypt); String interactiveJsonString = new String(decryptBase64, this.charset); InteractiveModel model = JSON.parseObject(interactiveJsonString, InteractiveModel.class); super.fullValue(model); } /** * 签名模型 * * @param tokenKey * @author Pluto * @since 2020-01-08 16:39:59 */ public void autographMsg(String tokenKey) { this.autograph = MD5Signature.getSignature(Charset.forName(this.charset), tokenKey, this.timestamp.toString(), this.encrypt, this.charset); } /** * 检查签名 * * @param tokenKey * @return * @author Pluto * @since 2020-01-08 16:40:09 */ public boolean checkAutograph(String tokenKey) { String signature = MD5Signature.getSignature(Charset.forName(this.charset), tokenKey, this.timestamp.toString(), this.encrypt, this.charset); return StringUtils.equals(this.autograph, signature); } /** * 填充消息 * * @param key * @param tokenKey * @throws Exception * @author Pluto * @since 2020-01-08 16:40:17 */ public void fullMessage(Key key, String tokenKey) throws Exception { this.timestamp = System.currentTimeMillis(); this.encryptMsg(key); this.autographMsg(tokenKey); } @Override public String toJSONString() { JSONObject jsonObject = new JSONObject(); jsonObject.put("charset", this.charset); jsonObject.put("timestamp", this.timestamp); jsonObject.put("encrypt", this.encrypt); jsonObject.put("autograph", this.autograph); return jsonObject.toJSONString(); } } ================================================ FILE: src/main/java/person/pluto/natcross2/model/enumeration/InteractiveTypeEnum.java ================================================ package person.pluto.natcross2.model.enumeration; import lombok.Getter; import org.apache.commons.lang3.StringUtils; /** ** 交互类型enum *
* * @author Pluto * @since 2019-07-17 09:50:33 */ @Getter public enum InteractiveTypeEnum { // UNKNOWN("未知"), // COMMON_REPLY("通用回复标签"), // HEART_TEST("发送心跳"), // HEART_TEST_REPLY("心跳测试回复"), // SERVER_WAIT_CLIENT("需求客户端建立连接"), // CLIENT_CONNECT("客户端建立通道连接"), // CLIENT_CONTROL("客户端控制端口建立连接"), // ; private final String describe; InteractiveTypeEnum(String describe) { this.describe = describe; } public static InteractiveTypeEnum getEnumByName(String name) { if (StringUtils.isBlank(name)) { return null; } for (InteractiveTypeEnum e : InteractiveTypeEnum.values()) { if (e.name().equals(name)) { return e; } } return null; } } ================================================ FILE: src/main/java/person/pluto/natcross2/model/enumeration/NatcrossResultEnum.java ================================================ package person.pluto.natcross2.model.enumeration; import org.apache.commons.lang3.StringUtils; import person.pluto.natcross2.model.NatcrossResultModel; /** ** 客户端服务端返回码 *
* * @author Pluto * @since 2019-03-28 10:59:53 */ public enum NatcrossResultEnum { // 成功 SUCCESS("1000", "成功"), // UNKNOWN_INTERACTIVE_TYPE("3001", "未知的通信类型"), // NO_HAS_SERVER_LISTEN("3002", "不存在请求的监听接口"), // 未知错误 FAIL("9999", "未知错误"); private final String code; private final String name; NatcrossResultEnum(String code, String name) { this.code = code; this.name = name; } public static NatcrossResultEnum getEnumByCode(String code) { if (StringUtils.isBlank(code)) { return null; } for (NatcrossResultEnum e : NatcrossResultEnum.values()) { if (e.code.equals(code)) { return e; } } return null; } public NatcrossResultModel toResultModel() { return NatcrossResultModel.of(this); } public String getCode() { return code; } public String getName() { return name; } } ================================================ FILE: src/main/java/person/pluto/natcross2/model/interactive/ClientConnectModel.java ================================================ package person.pluto.natcross2.model.interactive; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** ** 请求建立隧道模型 *
* * @author Pluto * @since 2020-01-08 16:36:58 */ @Data @NoArgsConstructor @AllArgsConstructor public class ClientConnectModel { private String socketPartKey; } ================================================ FILE: src/main/java/person/pluto/natcross2/model/interactive/ClientControlModel.java ================================================ package person.pluto.natcross2.model.interactive; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** ** 请求建立控制器模型 *
* * @author Pluto * @since 2020-01-08 16:37:12 */ @Data @NoArgsConstructor @AllArgsConstructor public class ClientControlModel { private Integer listenPort; } ================================================ FILE: src/main/java/person/pluto/natcross2/model/interactive/ServerWaitModel.java ================================================ package person.pluto.natcross2.model.interactive; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** ** 服务端等待建立隧道模型 *
* * @author Pluto * @since 2020-01-08 16:37:26 */ @Data @NoArgsConstructor @AllArgsConstructor public class ServerWaitModel { private String socketPartKey; } ================================================ FILE: src/main/java/person/pluto/natcross2/nio/INioProcessor.java ================================================ package person.pluto.natcross2.nio; import java.nio.channels.SelectionKey; /** ** nio 执行器 *
* * @author Pluto * @since 2021-04-12 17:51:37 */ @FunctionalInterface public interface INioProcessor { /** * 需要执行的方法 * * @param key */ void process(SelectionKey key); } ================================================ FILE: src/main/java/person/pluto/natcross2/nio/NioHallows.java ================================================ package person.pluto.natcross2.nio; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import person.pluto.natcross2.executor.NatcrossExecutor; import person.pluto.natcross2.utils.Assert; import person.pluto.natcross2.utils.CountWaitLatch; import java.io.IOException; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.util.Iterator; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; /** ** nio 容器 *
* * @author Pluto * @since 2021-04-13 09:25:51 */ @Slf4j @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class NioHallows implements Runnable { public static final NioHallows INSTANCE = new NioHallows(); /** * 注册监听动作 ** 要注意这里只拿最后的一次注册为准,即 {@code channel} 只能与一个 {@code proccesser} 动作对应 * * @param channel * @param ops 依据以下值进行或运算进行最后结果设定,并且 {@code channel} 要支持相应的动作 *
* - {@link SelectionKey#OP_ACCEPT} *
* - {@link SelectionKey#OP_CONNECT} *
* - {@link SelectionKey#OP_READ} *
* - {@link SelectionKey#OP_WRITE}
* @param processor 要执行的动作
* @throws IOException
* @author Pluto
* @since 2021-04-26 15:55:38
*/
public static void register(SelectableChannel channel, int ops, INioProcessor processor) throws IOException {
INSTANCE.register0(channel, ops, processor);
}
/**
* 根据 {@link SelectionKey} 恢复监听事件的注册
*
* @param key 原始的key
* @param ops 要与通过 {@link #register(SelectableChannel, int, INioProcessor)}
* 注册的事件统一
* @throws IOException
* @author Pluto
* @since 2021-05-07 13:21:49
*/
public static boolean reRegisterByKey(SelectionKey key, int ops) {
return INSTANCE.reRegisterByKey0(key, ops);
}
/**
* 释放注册
*
* @param channel
* @author Pluto
* @since 2021-04-26 16:03:51
*/
public static void release(SelectableChannel channel) {
INSTANCE.release0(channel);
}
private volatile Thread myThread = null;
private volatile boolean alive = false;
private volatile boolean canceled = false;
private volatile Selector selector;
private final Object selectorLock = new Object();
private final CountWaitLatch countWaitLatch = new CountWaitLatch();
private final Map
* 若 {@link #selector} 未有值,则会进行初始化:打开selector,并执行 {@link #start()}
*
* @return
* @throws IOException
* @author Pluto
* @since 2021-04-26 16:04:30
*/
public Selector getSelector() throws IOException {
// 判空、返回逻辑,按第一次取值进行,缺点是不能判断是否已经关闭,但与 this.cancel()
// 方法中的执行顺序来看,会先被设置为null,再去close,所以可以大概率认为若不为null即为没有关闭
Selector selector = this.selector;
if (Objects.isNull(selector)) {
synchronized (this.selectorLock) {
// 二次校验
// 若是主动退出,则不在创建,避免退出时有新任务而被重启,若要重新启用,则需要主动调用 start() 方法来启动
if (Objects.isNull(this.selector) && !this.canceled) {
this.selector = Selector.open();
this.start();
}
}
selector = this.selector;
if (Objects.isNull(selector)) {
throw new IOException("NioHallows's selector is closed");
}
}
return selector;
}
/**
* 获取唤醒后的 {@link #selector}
*
* 注意,若 {@link #run()} 快于你的任务,还是会被再次阻塞,只是执行了一次 {@link Selector#wakeup()}
*
* @return
* @throws IOException
* @author Pluto
* @since 2021-04-26 16:07:00
*/
public Selector getWakeupSelector() throws IOException {
return this.getSelector().wakeup();
}
/**
* 注册监听动作
*
* 要注意这里只拿最后的一次注册为准,即 {@code channel} 只能与一个 {@code proccesser} 动作对应
*
* @param channel
* @param ops 依据以下值进行或运算进行最后结果设定,并且 {@code channel} 要支持相应的动作
*
* - {@link SelectionKey#OP_ACCEPT}
*
* - {@link SelectionKey#OP_CONNECT}
*
* - {@link SelectionKey#OP_READ}
*
* - {@link SelectionKey#OP_WRITE}
* @param proccesser 要执行的动作
* @throws IOException
* @author Pluto
* @since 2021-04-26 15:55:38
*/
public void register0(SelectableChannel channel, int ops, INioProcessor proccesser) throws IOException {
Objects.requireNonNull(channel, "channel non null");
try {
this.channelProcesserMap.put(channel, ProcesserHolder.of(channel, ops, proccesser));
channel.configureBlocking(false);
this.countWaitLatch.countUp();
// 这里有个坑点,如果在select中,这里会被阻塞
channel.register(this.getWakeupSelector(), ops);
} catch (Throwable e) {
this.channelProcesserMap.remove(channel);
throw e;
} finally {
this.countWaitLatch.countDown();
}
}
/**
* 根据 {@link SelectionKey} 恢复监听事件的注册
*
* @param key 原始的key
* @param ops 要与通过 {@link #register0(SelectableChannel, int, INioProcessor)}
* 注册的事件统一
* @throws IOException
* @author Pluto
* @since 2021-05-07 13:21:49
*/
public boolean reRegisterByKey0(SelectionKey key, int ops) {
Objects.requireNonNull(key, "key non null");
Assert.state(key.selector() == this.selector, "this SelectionKey is not belong NioHallows's selector");
if (!key.isValid()) {
return false;
}
// 通过事件和源码分析,恢复注册是通过updateKeys.addLast进行,虽然没有被阻塞,但是需要进行一次唤醒才可以成功恢复事件监听
// 因无法获知是否成功注入selector,所以必须要进行一次唤醒操作,并且没有阻塞的问题,所以这里不通过countWaitLatch进行同步
key.interestOps(ops);
try {
this.getWakeupSelector();
} catch (IOException e) {
// 出错了交给其他的流程逻辑,这里只进行一次唤醒
}
return true;
}
/**
* 释放注册
*
* @param channel
* @author Pluto
* @since 2021-04-26 16:03:51
*/
public void release0(SelectableChannel channel) {
if (Objects.isNull(channel)) {
return;
}
this.channelProcesserMap.remove(channel);
SelectionKey key = channel.keyFor(this.selector);
if (Objects.nonNull(key)) {
key.cancel();
}
}
@Override
public void run() {
CountWaitLatch countWaitLatch = this.countWaitLatch;
while (this.alive) {
// 给注册事务一个时间,如果等待时间太长(可能需要注入的太多),就跳出再去获取新事件,防止饿死
try {
countWaitLatch.await(this.getWakeupSleepNanos(), TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
log.warn("selector wait register timeout");
}
Selector selector;
try {
selector = getSelector();
// 采用有期限的监听,以免线程太快,没有来的及注册,就永远阻塞在那里了
int select = selector.select(this.getSelectTimeout());
if (select <= 0) {
continue;
}
} catch (IOException e) {
log.error("NioHallows run exception", e);
continue;
}
Iterator
* 执行器暂存
*
* 客户端服务进程
*
* 默认的预读后处理适配器
*
* 客户端服务适配器
*
* 传值适配器的handler回复信息
*
* 预读后处理适配器
*
* 客户端服务配置
*
* 隧道过程加密的配置类
*
* 简单交互的客户端服务配置
*
* 默认的接收处理handler
*
* 传值方式客户端是配置的处理接口
*
* 常规接收处理handler
*
* 请求建立隧道处理器
*
* 请求建立控制器处理方法
*
* 处理方法接口
*
* 端口监听服务接口
*
* 转发监听服务控制类
*
* 监听转发服务进程
*
* 清理无效端口
*
* 清理无效端口 线程
*
* 交互及隧道都加密
*
* 穿透监听服务配置
*
* 多客户端;监听服务配置
*
* 交互加密配置
*
* 简单的交互、隧道;监听服务配置
*
* 控制socket实例
*
* 控制端口接口
*
* 适配多客户端模式,若是替换关闭则可能不进行关闭
* 实现的类需要自己进行幂等性处理
*/
void startRecv();
/**
* 设置控制的监听线程
*
* @param serverListen
*/
void setServerListen(IServerListen serverListen);
}
================================================
FILE: src/main/java/person/pluto/natcross2/serverside/listen/control/MultiControlSocket.java
================================================
package person.pluto.natcross2.serverside.listen.control;
import person.pluto.natcross2.serverside.listen.IServerListen;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Objects;
/**
*
* 复合 控制socket实例
*
* 心跳检测
*
* 统一回复 处理器
*
* 接收处理器
*
* 创建服务端口接口
*
* AES加解密工具
*
* 断言
*
* 可增 计数 门闩
*
* MD5散列签名
*
* 无归类的工具集
*
* 若是传统1对1模式,则等价调用 {@link #close()}
*
* @since 2.3
*/
default void replaceClose() {
this.close();
}
/**
* 开启接收线程
*