diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 00000000..bd78f23b --- /dev/null +++ b/.cursorignore @@ -0,0 +1,4 @@ +# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) +.idea +distribution +*.zip \ No newline at end of file diff --git a/.gitignore b/.gitignore index a063834e..c266ed62 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,7 @@ .* # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* +/test.sh +*/logs/ +*/target/ +*.zip diff --git a/README.md b/README.md index 34b497d2..6aded7ce 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ https://nat.nioee.com - ## Lanproxy个人升级版 核心功能: @@ -19,7 +18,7 @@ https://nat.nioee.com 体验地址 https://lanp.nioee.com (测试用户名密码 test/123456) -![panel](panel.png) +![panel](assets/panel.png) ## Lanproxy开源免费版 @@ -37,8 +36,8 @@ lanproxy是一个将局域网个人电脑、服务器代理到公网的内网穿 #### 获取发布包 -- 拉取源码,运行 mvn package,打包后的资源放在distribution目录中,包括client和server -- 或直接下载发布包 https://github.com/ffay/lanproxy/releases +- 拉取源码,运行 mvn package,打包后的资源放在distribution目录中,包括client和server +- 或直接下载发布包 https://github.com/ffay/lanproxy/releases #### 配置 @@ -48,10 +47,8 @@ server的配置文件放置在conf目录中,配置 config.properties ```properties server.bind=0.0.0.0 - #与代理客户端通信端口 server.port=4900 - #ssl相关配置 server.ssl.enable=true server.ssl.bind=0.0.0.0 @@ -59,10 +56,8 @@ server.ssl.port=4993 server.ssl.jksPath=test.jks server.ssl.keyStorePassword=123456 server.ssl.keyManagerPassword=123456 - #这个配置可以忽略 server.ssl.needsClientAuth=false - #WEB在线配置管理相关信息 config.server.bind=0.0.0.0 config.server.port=8090 @@ -72,11 +67,14 @@ config.admin.password=admin 代理配置,打开地址 http://ip:8090 ,使用上面配置中配置的用户名密码登录,进入如下代理配置界面 -![webconfig](readme_zh_client_list.png) +![webconfig](assets/readme_zh_client_list.png) -![webconfig](readme_zh_proxy_list.png) +![webconfig](assets/readme_zh_proxy_list.png) -![webconfig](readme_zh_stat_list.png) +![webconfig](assets/readme_zh_stat_list.png) + +TCP请求包内容记录并在网页上展示: +![RequestLogShow](assets/RequestLog.png) > 一个server可以支持多个客户端连接 > 配置数据存放在 ~/.lanproxy/config.json 文件中 @@ -92,10 +90,8 @@ client.key= ssl.enable=true ssl.jksPath=test.jks ssl.keyStorePassword=123456 - #这里填写实际的proxy-server地址;没有服务器默认即可,自己有服务器的更换为自己的proxy-server(IP)地址 server.host=lp.thingsglobal.org - #proxy-server ssl默认端口4993,默认普通端口4900 #ssl.enable=true时这里填写ssl端口,ssl.enable=false时这里填写普通端口 server.port=4993 @@ -145,5 +141,51 @@ nohup ./client_linux_amd64 -s SERVER_IP -p SERVER_SSL_PORT -k CLIENT_KEY -ssl tr #### 其他 -- 在家里使用公司的网络,可以和 https://github.com/ffay/http-proxy-server 这个http代理项目配合使用(个人升级版已经内置代理上网功能,详细资料 https://file.nioee.com/f/76ebbce67c864e4dbe7e/ ) +- 在家里使用公司的网络,可以和 https://github.com/ffay/http-proxy-server + 这个http代理项目配合使用(个人升级版已经内置代理上网功能,详细资料 https://file.nioee.com/f/76ebbce67c864e4dbe7e/ ) - 对于正常网站,80和443端口只有一个,可以购买个人升级版本解决端口复用问题 + +## 生成 JSK 文件 + +```shell +keytool -genkey -keyalg RSA -keysize 2048 -validity 365 -dname "CN=test, OU=test,O=test, L=shanghai, ST=shanghai, C=CN" -alias csii_key -keypass 888888 -keystore sdm-202212011626.jks -storepass 123456 +``` + +### ⚠️注️: + +> 1. 当-keypass 和 -storepass 设为不同时,keytool会给出以下警告: +> ```shell +> 警告: PKCS12 密钥库不支持其他存储和密钥口令。正在忽略用户指定的-keypass值。 +> ``` +> 这时在configuration.properties文件中keyStore和keyManager密码必须为一直是kesStore的密码。 +> ```properties +> server.ssl.keyStorePassword=123456(keystore密码) +> server.ssl.keyManagerPassword=123456(keystore密码) +> ``` +> 2.提高安全性: keysize必须为大于等于2048 +> ```shell +> 生成的证书 使用的 1024 位 RSA 密钥 被视为存在安全风险。此密钥大小将在未来的更新中被禁用。 +>``` +> 详情请见 [“陷阱”素数(‘trapdoored’ primes)的出现,使用1024位密钥加密算法已不再安全](https://searchsecurity.techtarget.com.cn/11-24358/) + +### 进阶版 + +#### 映射配置 + +![webconfig](assets/进阶版配置表单截图.png) + +### 开发&调试 + +如果想在IDEA中运行并调试,务必要配置Application Configuration +![img.png](assets/application-configuration-shortcut.png) +JVM Options: +```shell +-Dapp.home=<项目的绝对路径>/proxy-server +-Djava.awt.headless=true +-Djava.net.preferIPv4Stack=true +-Djdk.tls.rejectClientInitiatedRenegotiation=true +-Xdebug +-Xnoagent +-Djava.compiler=NONE +-Xrunjdwp:transport=dt_socket,address=12000,server=y,suspend=n +``` \ No newline at end of file diff --git a/README_en.md b/README_en.md index c0f0dcd1..d7aac2a0 100644 --- a/README_en.md +++ b/README_en.md @@ -19,7 +19,7 @@ Lanproxy is a reverse proxy to help you expose a local server behind a NAT or fi - Run personal cloud services from your own private network ### Architecture -![lanproxy](lanproxy.png) +![lanproxy](assets/lanproxy.png) ### Configure @@ -52,11 +52,11 @@ config.admin.password=admin > Visit your config web service using url http://ip:8090 -![webconfig](readme_en_client_list.png) +![webconfig](assets/readme_en_client_list.png) -![webconfig](readme_en_proxy_list.png) +![webconfig](assets/readme_en_proxy_list.png) -![webconfig](readme_en_stat_list.png) +![webconfig](assets/readme_en_stat_list.png) #### client diff --git a/assets/RequestLog.png b/assets/RequestLog.png new file mode 100644 index 00000000..8d7e7fe2 Binary files /dev/null and b/assets/RequestLog.png differ diff --git a/assets/application-configuration-shortcut.png b/assets/application-configuration-shortcut.png new file mode 100644 index 00000000..27848c79 Binary files /dev/null and b/assets/application-configuration-shortcut.png differ diff --git a/lanproxy.png b/assets/lanproxy.png similarity index 100% rename from lanproxy.png rename to assets/lanproxy.png diff --git a/panel.png b/assets/panel.png similarity index 100% rename from panel.png rename to assets/panel.png diff --git a/readme_en_client_list.png b/assets/readme_en_client_list.png similarity index 100% rename from readme_en_client_list.png rename to assets/readme_en_client_list.png diff --git a/readme_en_proxy_list.png b/assets/readme_en_proxy_list.png similarity index 100% rename from readme_en_proxy_list.png rename to assets/readme_en_proxy_list.png diff --git a/readme_en_stat_list.png b/assets/readme_en_stat_list.png similarity index 100% rename from readme_en_stat_list.png rename to assets/readme_en_stat_list.png diff --git a/readme_zh_client_list.png b/assets/readme_zh_client_list.png similarity index 100% rename from readme_zh_client_list.png rename to assets/readme_zh_client_list.png diff --git a/readme_zh_proxy_list.png b/assets/readme_zh_proxy_list.png similarity index 100% rename from readme_zh_proxy_list.png rename to assets/readme_zh_proxy_list.png diff --git a/assets/readme_zh_stat_list.png b/assets/readme_zh_stat_list.png new file mode 100644 index 00000000..361adb9a Binary files /dev/null and b/assets/readme_zh_stat_list.png differ diff --git "a/assets/\350\277\233\351\230\266\347\211\210\351\205\215\347\275\256\350\241\250\345\215\225\346\210\252\345\233\276.png" "b/assets/\350\277\233\351\230\266\347\211\210\351\205\215\347\275\256\350\241\250\345\215\225\346\210\252\345\233\276.png" new file mode 100644 index 00000000..ee3761d8 Binary files /dev/null and "b/assets/\350\277\233\351\230\266\347\211\210\351\205\215\347\275\256\350\241\250\345\215\225\346\210\252\345\233\276.png" differ diff --git a/lanproxy.iml b/lanproxy.iml new file mode 100644 index 00000000..2763491b --- /dev/null +++ b/lanproxy.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 264ce4c6..2cf7f647 100644 --- a/pom.xml +++ b/pom.xml @@ -12,6 +12,8 @@ UTF-8 UTF-8 UTF-8 + 1.8 + 1.8 @@ -49,8 +51,6 @@ maven-compiler-plugin 3.1 - 1.7 - 1.7 UTF-8 diff --git a/proxy-client/src/main/java/org/fengfei/lanproxy/client/ClientChannelMannager.java b/proxy-client/src/main/java/org/fengfei/lanproxy/client/ClientChannelMannager.java index 6860871f..4f980f02 100644 --- a/proxy-client/src/main/java/org/fengfei/lanproxy/client/ClientChannelMannager.java +++ b/proxy-client/src/main/java/org/fengfei/lanproxy/client/ClientChannelMannager.java @@ -17,7 +17,7 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelOption; import io.netty.util.AttributeKey; /** diff --git a/proxy-client/src/main/resources/config.properties b/proxy-client/src/main/resources/config.properties index b48b6e2f..29a7cbac 100644 --- a/proxy-client/src/main/resources/config.properties +++ b/proxy-client/src/main/resources/config.properties @@ -1,9 +1,9 @@ -client.key=client +client.key=7815c64fbd02420c84c347704a8967b5 ssl.enable=false ssl.jksPath=test.jks ssl.keyStorePassword=123456 -server.host=127.0.0.1 +server.host=192.168.31.115 #default ssl port is 4993 server.port=4900 \ No newline at end of file diff --git a/proxy-server/pom.xml b/proxy-server/pom.xml index c0a24b4e..abc92a9d 100644 --- a/proxy-server/pom.xml +++ b/proxy-server/pom.xml @@ -9,6 +9,7 @@ proxy-server jar proxy-server + ${parent.version}.1 http://maven.apache.org diff --git a/proxy-server/proxy-server.iml b/proxy-server/proxy-server.iml new file mode 100644 index 00000000..e0b14e33 --- /dev/null +++ b/proxy-server/proxy-server.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/proxy-server/src/main/java/org/fengfei/lanproxy/server/ProxyServerContainer.java b/proxy-server/src/main/java/org/fengfei/lanproxy/server/ProxyServerContainer.java index 6a9c8c53..9a74cc20 100644 --- a/proxy-server/src/main/java/org/fengfei/lanproxy/server/ProxyServerContainer.java +++ b/proxy-server/src/main/java/org/fengfei/lanproxy/server/ProxyServerContainer.java @@ -128,6 +128,8 @@ private void startUserPort() { @Override public void initChannel(SocketChannel ch) throws Exception { +// // TODO: 这里实现一个流量和访问日志的记录 + logger.info("{}===>{}", ch.remoteAddress(), ch.localAddress()); ch.pipeline().addFirst(new BytesMetricsHandler()); ch.pipeline().addLast(new UserChannelHandler()); } @@ -171,7 +173,7 @@ private ChannelHandler createSslHandler(SSLContext sslContext, boolean needsClie } public static void main(String[] args) { - ContainerHelper.start(Arrays.asList(new Container[] { new ProxyServerContainer(), new WebConfigContainer() })); + ContainerHelper.start(Arrays.asList(new Container[]{new ProxyServerContainer(), new WebConfigContainer()})); } } diff --git a/proxy-server/src/main/java/org/fengfei/lanproxy/server/config/web/routes/RouteConfig.java b/proxy-server/src/main/java/org/fengfei/lanproxy/server/config/web/routes/RouteConfig.java index bb05ce8c..f119fb6f 100644 --- a/proxy-server/src/main/java/org/fengfei/lanproxy/server/config/web/routes/RouteConfig.java +++ b/proxy-server/src/main/java/org/fengfei/lanproxy/server/config/web/routes/RouteConfig.java @@ -15,6 +15,7 @@ import org.fengfei.lanproxy.server.config.web.ResponseInfo; import org.fengfei.lanproxy.server.config.web.exception.ContextException; import org.fengfei.lanproxy.server.metrics.MetricsCollector; +import org.fengfei.lanproxy.server.requestlogs.RequestLogCollector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -170,6 +171,14 @@ public ResponseInfo request(FullHttpRequest request) { return ResponseInfo.build(MetricsCollector.getAndResetAllMetrics()); } }); + // 添加获取日志的路由 + ApiRoute.addRoute("/api/logs", new RequestHandler() { + + @Override + public ResponseInfo request(FullHttpRequest request) { + return ResponseInfo.build(ResponseInfo.CODE_OK, "success", RequestLogCollector.getRecentLogs()); + } + }); } } diff --git a/proxy-server/src/main/java/org/fengfei/lanproxy/server/handlers/RequestInterceptor.java b/proxy-server/src/main/java/org/fengfei/lanproxy/server/handlers/RequestInterceptor.java new file mode 100644 index 00000000..86fe28a4 --- /dev/null +++ b/proxy-server/src/main/java/org/fengfei/lanproxy/server/handlers/RequestInterceptor.java @@ -0,0 +1,86 @@ +package org.fengfei.lanproxy.server.handlers; + +import io.netty.channel.Channel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetSocketAddress; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +public class RequestInterceptor { + + private static final int MAX_REQUESTS_PER_SECOND = 100; // 每秒最大请求数 + private static final int BLACK_LIST_THRESHOLD = 1000; // 加入黑名单阈值 + private static final Logger logger = LoggerFactory.getLogger(RequestInterceptor.class); + + // 使用ConcurrentHashMap存储IP访问统计 + private final ConcurrentHashMap statsMap = new ConcurrentHashMap<>(); + + // IP黑名单 + private final Set blackList = ConcurrentHashMap.newKeySet(); + + public boolean interceptRequest(Channel channel) { + InetSocketAddress address = (InetSocketAddress) channel.remoteAddress(); + String ip = address.getAddress().getHostAddress(); + + // 检查是否在黑名单中 + if (blackList.contains(ip)) { + logger.warn("Blocked request from blacklisted IP: {}", ip); + return true; + } + + // 获取或创建访问统计 + AccessStats stats = statsMap.computeIfAbsent(ip, k -> new AccessStats()); + + // 检查频率限制 + if (stats.isExceedingLimit()) { + stats.incrementBlockCount(); + + // 超过阈值加入黑名单 + if (stats.getBlockCount() > BLACK_LIST_THRESHOLD) { + blackList.add(ip); + logger.warn("Added IP to blacklist: {}", ip); + } + + return true; + } + + // 记录请求 + stats.recordRequest(); + return false; + } + + private static class AccessStats { + private AtomicInteger requestCount = new AtomicInteger(0); + private AtomicInteger blockCount = new AtomicInteger(0); + private long lastResetTime = System.currentTimeMillis(); + + public boolean isExceedingLimit() { + resetIfNeeded(); + return requestCount.get() >= MAX_REQUESTS_PER_SECOND; + } + + public void recordRequest() { + resetIfNeeded(); + requestCount.incrementAndGet(); + } + + public void incrementBlockCount() { + blockCount.incrementAndGet(); + } + + public int getBlockCount() { + return blockCount.get(); + } + + private void resetIfNeeded() { + long now = System.currentTimeMillis(); + if (now - lastResetTime > 1000) { + requestCount.set(0); + lastResetTime = now; + } + } + } +} \ No newline at end of file diff --git a/proxy-server/src/main/java/org/fengfei/lanproxy/server/handlers/RequestLogHandler.java b/proxy-server/src/main/java/org/fengfei/lanproxy/server/handlers/RequestLogHandler.java new file mode 100644 index 00000000..0dc962f2 --- /dev/null +++ b/proxy-server/src/main/java/org/fengfei/lanproxy/server/handlers/RequestLogHandler.java @@ -0,0 +1,94 @@ +package org.fengfei.lanproxy.server.handlers; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import org.fengfei.lanproxy.server.requestlogs.RequestLog; +import org.fengfei.lanproxy.server.requestlogs.RequestLogCollector; +import org.fengfei.lanproxy.server.utils.PacketParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetSocketAddress; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +public class RequestLogHandler { + private static final Logger logger = LoggerFactory.getLogger(RequestLogHandler.class); + + // 使用队列异步处理日志 + private final BlockingQueue logQueue = new LinkedBlockingQueue<>(); + + public void logRequest(Channel channel, String userId, ByteBuf data) { + RequestLog log = new RequestLog(); + InetSocketAddress address = (InetSocketAddress) channel.remoteAddress(); + log.setIp(address.getAddress().getHostAddress()); + log.setPort(address.getPort()); + log.setTime(new Date()); + log.setUserId(userId); + logger.info("buf.readableBytes(): {}", data.readableBytes()); + log.setData(data); + + // 异步写入日志队列 + logQueue.offer(log); + } + + // 日志处理线程 + public class LogProcessor implements Runnable { + public void run() { + while (true) { + try { + RequestLog log = logQueue.take(); + // 使用新的包解析器解析请求 + PacketParser.PacketInfo packetInfo = PacketParser.parsePacket(log.getData()); + log.setRequestInfo(packetInfo.toString()); + // 写入日志 + logger.info("Request from {}:{} - {}", log.getIp(), log.getPort(), packetInfo.getProtocol()); + // 修改日志处理逻辑,保存最近的日志 + saveLog(log); + } catch (Exception e) { + logger.error("Error processing log", e); + } + } + } + } + + // 修改日志处理逻辑,保存最近的日志 + private void saveLog(RequestLog log) { + RequestLogCollector.saveLog(log); + } + + + // 添加日志VO类用于页面展示 + public static class RequestLogVO { + private String ip; + private int port; + private String time; + private String requestInfo; + + public RequestLogVO(RequestLog log) { + this.ip = log.getIp(); + this.port = log.getPort(); + this.time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(log.getTime()); + this.requestInfo = log.getRequestInfo(); + } + + // getter方法 + public String getIp() { + return ip; + } + + public int getPort() { + return port; + } + + public String getTime() { + return time; + } + + public String getRequestInfo() { + return requestInfo; + } + } +} diff --git a/proxy-server/src/main/java/org/fengfei/lanproxy/server/handlers/ServerChannelHandler.java b/proxy-server/src/main/java/org/fengfei/lanproxy/server/handlers/ServerChannelHandler.java index 59aff7b9..f7273626 100644 --- a/proxy-server/src/main/java/org/fengfei/lanproxy/server/handlers/ServerChannelHandler.java +++ b/proxy-server/src/main/java/org/fengfei/lanproxy/server/handlers/ServerChannelHandler.java @@ -18,17 +18,19 @@ import io.netty.channel.SimpleChannelInboundHandler; /** - * * @author fengfei - * */ public class ServerChannelHandler extends SimpleChannelInboundHandler { - private static Logger logger = LoggerFactory.getLogger(ServerChannelHandler.class); + private static final Logger logger = LoggerFactory.getLogger(ServerChannelHandler.class); + + public ServerChannelHandler() { + } @Override protected void channelRead0(ChannelHandlerContext ctx, ProxyMessage proxyMessage) throws Exception { logger.debug("ProxyMessage received {}", proxyMessage.getType()); + // 原有的请求处理逻辑 switch (proxyMessage.getType()) { case ProxyMessage.TYPE_HEARTBEAT: handleHeartbeatMessage(ctx, proxyMessage); diff --git a/proxy-server/src/main/java/org/fengfei/lanproxy/server/handlers/UserChannelHandler.java b/proxy-server/src/main/java/org/fengfei/lanproxy/server/handlers/UserChannelHandler.java index 4a80ab7c..51f598df 100755 --- a/proxy-server/src/main/java/org/fengfei/lanproxy/server/handlers/UserChannelHandler.java +++ b/proxy-server/src/main/java/org/fengfei/lanproxy/server/handlers/UserChannelHandler.java @@ -13,13 +13,25 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelOption; import io.netty.channel.SimpleChannelInboundHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * 处理服务端 channel. */ public class UserChannelHandler extends SimpleChannelInboundHandler { + private static final Logger logger = LoggerFactory.getLogger(UserChannelHandler.class); - private static AtomicLong userIdProducer = new AtomicLong(0); + private static final AtomicLong userIdProducer = new AtomicLong(0); + private RequestInterceptor interceptor = new RequestInterceptor(); + public RequestLogHandler requestLogHandler = new RequestLogHandler(); + + public UserChannelHandler() { + // 启动日志处理线程 + Thread logThread = new Thread(requestLogHandler.new LogProcessor()); + logThread.setDaemon(true); + logThread.start(); + } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { @@ -39,9 +51,17 @@ protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Excep // 该端口还没有代理客户端 ctx.channel().close(); } else { + String userId = ProxyChannelManager.getUserChannelUserId(userChannel); + logger.info("buf.readableBytes(): {}", buf.readableBytes()); + requestLogHandler.logRequest(ctx.channel(), "UserId:" + userId, buf.copy()); byte[] bytes = new byte[buf.readableBytes()]; buf.readBytes(bytes); - String userId = ProxyChannelManager.getUserChannelUserId(userChannel); + // 记录请求 + // 拦截检查 + if (interceptor.interceptRequest(ctx.channel())) { + ctx.close(); + return; + } ProxyMessage proxyMessage = new ProxyMessage(); proxyMessage.setType(ProxyMessage.P_TYPE_TRANSFER); proxyMessage.setUri(userId); diff --git a/proxy-server/src/main/java/org/fengfei/lanproxy/server/metrics/MetricsCollector.java b/proxy-server/src/main/java/org/fengfei/lanproxy/server/metrics/MetricsCollector.java index 2b79dc42..84611bee 100644 --- a/proxy-server/src/main/java/org/fengfei/lanproxy/server/metrics/MetricsCollector.java +++ b/proxy-server/src/main/java/org/fengfei/lanproxy/server/metrics/MetricsCollector.java @@ -24,6 +24,10 @@ public class MetricsCollector { private AtomicLong wroteMsgs = new AtomicLong(); private AtomicInteger channels = new AtomicInteger(); + /* + 开始记录时间(每次重启时会重置) + */ + private AtomicLong collectAt = new AtomicLong(System.currentTimeMillis()); private MetricsCollector() { } @@ -70,10 +74,10 @@ public Metrics getAndResetMetrics() { metrics.setPort(port); metrics.setReadBytes(readBytes.getAndSet(0)); metrics.setWroteBytes(wroteBytes.getAndSet(0)); - metrics.setTimestamp(System.currentTimeMillis()); metrics.setReadMsgs(readMsgs.getAndSet(0)); metrics.setWroteMsgs(wroteMsgs.getAndSet(0)); - + metrics.setTimestamp(collectAt.getAndSet(System.currentTimeMillis())); + System.out.println("collectAt:" + metrics.getTimestamp() + " | " + collectAt.get()); return metrics; } @@ -83,9 +87,9 @@ public Metrics getMetrics() { metrics.setPort(port); metrics.setReadBytes(readBytes.get()); metrics.setWroteBytes(wroteBytes.get()); - metrics.setTimestamp(System.currentTimeMillis()); metrics.setReadMsgs(readMsgs.get()); metrics.setWroteMsgs(wroteMsgs.get()); + metrics.setTimestamp(collectAt.get()); return metrics; } diff --git a/proxy-server/src/main/java/org/fengfei/lanproxy/server/requestlogs/RequestLog.java b/proxy-server/src/main/java/org/fengfei/lanproxy/server/requestlogs/RequestLog.java new file mode 100644 index 00000000..ce428139 --- /dev/null +++ b/proxy-server/src/main/java/org/fengfei/lanproxy/server/requestlogs/RequestLog.java @@ -0,0 +1,63 @@ +package org.fengfei.lanproxy.server.requestlogs; + +import io.netty.buffer.ByteBuf; + +import java.util.Date; + +//定义日志对象 +public class RequestLog { + private String ip; + private int port; + private Date time; + private String userId; + private ByteBuf data; + private String requestInfo; + + public void setIp(String ip) { + this.ip = ip; + } + + public void setPort(int port) { + this.port = port; + } + + public void setTime(Date time) { + this.time = time; + } + + public void setRequestInfo(String requestInfo) { + this.requestInfo = requestInfo; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getIp() { + return ip; + } + + public int getPort() { + return port; + } + + public String getRequestInfo() { + return requestInfo; + } + + public String getUserId() { + return userId; + } + + public Date getTime() { + return time; + } + + public ByteBuf getData() { + return data; + } + + public void setData(ByteBuf data) { + this.data = data; + } +} \ No newline at end of file diff --git a/proxy-server/src/main/java/org/fengfei/lanproxy/server/requestlogs/RequestLogCollector.java b/proxy-server/src/main/java/org/fengfei/lanproxy/server/requestlogs/RequestLogCollector.java new file mode 100644 index 00000000..0fac7213 --- /dev/null +++ b/proxy-server/src/main/java/org/fengfei/lanproxy/server/requestlogs/RequestLogCollector.java @@ -0,0 +1,33 @@ +package org.fengfei.lanproxy.server.requestlogs; + +import org.fengfei.lanproxy.server.handlers.RequestLogHandler; + +import java.util.ArrayList; +import java.util.List; + +public class RequestLogCollector { + private static final List recentLogs = new ArrayList<>(); + // 存储最近的日志记录 + private static final int MAX_LOG_SIZE = 1000; + + // 添加获取日志的方法 + public static List getRecentLogs() { + List logs = new ArrayList<>(); + synchronized (recentLogs) { + for (RequestLog log : recentLogs) { + logs.add(new RequestLogHandler.RequestLogVO(log)); + } + } + return logs; + } + + // 修改日志处理逻辑,保存最近的日志 + public static void saveLog(RequestLog log) { + synchronized (recentLogs) { + if (recentLogs.size() >= MAX_LOG_SIZE) { + recentLogs.remove(0); + } + recentLogs.add(log); + } + } +} diff --git a/proxy-server/src/main/java/org/fengfei/lanproxy/server/utils/PacketParser.java b/proxy-server/src/main/java/org/fengfei/lanproxy/server/utils/PacketParser.java new file mode 100644 index 00000000..d716e628 --- /dev/null +++ b/proxy-server/src/main/java/org/fengfei/lanproxy/server/utils/PacketParser.java @@ -0,0 +1,285 @@ +package org.fengfei.lanproxy.server.utils; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.HttpMethod; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +public class PacketParser { + private static final Logger logger = LoggerFactory.getLogger(PacketParser.class); + + public static class PacketInfo { + private String protocol; + private Map headers = new HashMap<>(); + private String rawData; + private Map attributes = new HashMap<>(); + + // Getters and setters + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public Map getHeaders() { + return headers; + } + + public String getRawData() { + return rawData; + } + + public void setRawData(String rawData) { + this.rawData = rawData; + } + + public Map getAttributes() { + return attributes; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(protocol).append(" - "); + + if ("SSH".equals(protocol)) { + sb.append("Type: ").append(attributes.get("messageType")); + if (attributes.containsKey("version")) { + sb.append(", Version: ").append(attributes.get("version")); + } + if (attributes.containsKey("software")) { + sb.append(", Client: ").append(attributes.get("software")); + } + } else if ("PostgreSQL".equals(protocol)) { + sb.append("Type: ").append(attributes.get("messageType")); + if ("StartupMessage".equals(attributes.get("messageType"))) { + sb.append(", Version: ").append(attributes.get("protocolVersion")); + @SuppressWarnings("unchecked") + Map params = (Map) attributes.get("parameters"); + if (params != null) { + sb.append("\nParameters:"); + params.forEach((k, v) -> sb.append("\n ").append(k).append(": ").append(v)); + } + } + } else if (protocol.startsWith("HTTP")) { + sb.append(attributes.get("method")).append(" ") + .append(attributes.get("path")).append("\n"); + if (headers.containsKey("User-Agent")) { + sb.append("User-Agent: ").append(headers.get("User-Agent")).append("\n"); + } + if (headers.containsKey("Content-Type")) { + sb.append("Content-Type: ").append(headers.get("Content-Type")); + } + } else { + sb.append("Size: ").append(attributes.get("size")).append(" bytes"); + if (attributes.containsKey("preview")) { + sb.append("\nPreview: ").append(attributes.get("preview")); + } + } + return sb.toString(); + } + } + + public static PacketInfo parsePacket(ByteBuf buf) { + PacketInfo info = new PacketInfo(); + if (buf == null || buf.readableBytes() == 0) { + info.setProtocol("UNKNOWN"); + return info; + } + + // 检测协议 + if (isSSHPacket(buf)) { + parseSSHPacket(buf, info); + } else if (isHttpRequest(buf)) { + parseHttpRequest(buf, info); + } else if (isSslRequest(buf)) { + parseSslRequest(buf, info); + } else if (isPostgreSQLPacket(buf)) { + parsePostgreSQLPacket(buf, info); + } else if (isDnsPacket(buf)) { + parseDnsPacket(buf, info); + } else { + parseGenericPacket(buf, info); + } + + return info; + } + + private static void parseHttpRequest(ByteBuf buf, PacketInfo info) { + String content = buf.toString(StandardCharsets.UTF_8); + String[] lines = content.split("\r\n"); + + // 解析请求行 + String[] requestLine = lines[0].split(" "); + info.setProtocol("HTTP"); + info.getAttributes().put("method", requestLine[0]); + info.getAttributes().put("path", requestLine[1]); + info.getAttributes().put("version", requestLine[2]); + + // 解析请求头 + for (int i = 1; i < lines.length; i++) { + String line = lines[i]; + if (line.isEmpty()) break; + + int colonIdx = line.indexOf(':'); + if (colonIdx > 0) { + String key = line.substring(0, colonIdx).trim(); + String value = line.substring(colonIdx + 1).trim(); + info.getHeaders().put(key, value); + } + } + } + + private static void parseSslRequest(ByteBuf buf, PacketInfo info) { + info.setProtocol("HTTPS"); + info.getAttributes().put("size", buf.readableBytes()); + info.getAttributes().put("sslVersion", buf.getByte(buf.readerIndex() + 1) & 0xFF); + } + + private static void parseDnsPacket(ByteBuf buf, PacketInfo info) { + info.setProtocol("DNS"); + info.getAttributes().put("size", buf.readableBytes()); + // 解析DNS包头 + if (buf.readableBytes() >= 12) { + info.getAttributes().put("transactionId", buf.getShort(buf.readerIndex())); + info.getAttributes().put("flags", buf.getShort(buf.readerIndex() + 2)); + } + } + + private static void parseGenericPacket(ByteBuf buf, PacketInfo info) { + info.setProtocol("TCP/UDP"); + info.getAttributes().put("size", buf.readableBytes()); + + // 生成16进制预览 + int previewSize = Math.min(16, buf.readableBytes()); + byte[] preview = new byte[previewSize]; + buf.getBytes(buf.readerIndex(), preview); + StringBuilder hexPreview = new StringBuilder(); + StringBuilder asciiPreview = new StringBuilder(); + + for (byte b : preview) { + hexPreview.append(String.format("%02X ", b)); + asciiPreview.append(isPrintable(b) ? (char) b : '.'); + } + + info.getAttributes().put("preview", + String.format("HEX: %s | ASCII: %s", hexPreview, asciiPreview)); + } + + private static boolean isPostgreSQLPacket(ByteBuf buf) { + if (buf.readableBytes() < 8) return false; + + // PostgreSQL 启动消息的长度至少为8字节 + int messageLength = buf.getInt(buf.readerIndex()); + if (messageLength < 8) return false; + + // 检查协议版本号(196608 = 3.0) + int protocolVersion = buf.getInt(buf.readerIndex() + 4); + return protocolVersion == 196608 || protocolVersion == 80877103; // 80877103是SSLRequest + } + + private static void parsePostgreSQLPacket(ByteBuf buf, PacketInfo info) { + info.setProtocol("PostgreSQL"); + int messageLength = buf.getInt(buf.readerIndex()); + int protocolVersion = buf.getInt(buf.readerIndex() + 4); + + info.getAttributes().put("messageLength", messageLength); + + if (protocolVersion == 80877103) { + info.getAttributes().put("messageType", "SSLRequest"); + } else { + info.getAttributes().put("messageType", "StartupMessage"); + info.getAttributes().put("protocolVersion", "3.0"); + + // 尝试解析参数 + if (buf.readableBytes() > 8) { + Map params = new HashMap<>(); + int offset = 8; + StringBuilder paramBuilder = new StringBuilder(); + + while (offset < messageLength - 1) { + paramBuilder.setLength(0); + while (buf.getByte(buf.readerIndex() + offset) != 0) { + paramBuilder.append((char) buf.getByte(buf.readerIndex() + offset)); + offset++; + } + String key = paramBuilder.toString(); + offset++; // 跳过null字节 + + paramBuilder.setLength(0); + while (offset < messageLength - 1 && buf.getByte(buf.readerIndex() + offset) != 0) { + paramBuilder.append((char) buf.getByte(buf.readerIndex() + offset)); + offset++; + } + String value = paramBuilder.toString(); + offset++; // 跳过null字节 + + if (!key.isEmpty()) { + params.put(key, value); + } + } + + info.getAttributes().put("parameters", params); + } + } + } + + private static boolean isSSHPacket(ByteBuf buf) { + if (buf.readableBytes() < 4) return false; + + // SSH协议以"SSH-"开头 + byte[] header = new byte[4]; + buf.getBytes(buf.readerIndex(), header); + return new String(header).equals("SSH-"); + } + + private static void parseSSHPacket(ByteBuf buf, PacketInfo info) { + info.setProtocol("SSH"); + + // 读取SSH版本信息 + String content = buf.toString(StandardCharsets.UTF_8); + String[] parts = content.split("-", 3); + if (parts.length >= 3) { + // 格式通常是: SSH-2.0-OpenSSH_8.1 + String version = parts[1]; + String software = parts[2].split("\\r?\\n")[0]; + + info.getAttributes().put("version", version); + info.getAttributes().put("software", software); + info.getAttributes().put("messageType", "Version Exchange"); + } + + info.getAttributes().put("size", buf.readableBytes()); + } + + // 其他辅助方法... + private static boolean isHttpRequest(ByteBuf buf) { + if (buf.readableBytes() < 4) return false; + byte[] firstBytes = new byte[4]; + buf.getBytes(buf.readerIndex(), firstBytes); + String method = new String(firstBytes, StandardCharsets.UTF_8); + return method.startsWith("GET ") || method.startsWith("POST") || + method.startsWith("PUT ") || method.startsWith("HEAD") || + method.startsWith("DELE"); + } + + private static boolean isSslRequest(ByteBuf buf) { + return buf.readableBytes() >= 5 && + buf.getByte(buf.readerIndex()) == 0x16; + } + + private static boolean isDnsPacket(ByteBuf buf) { + return buf.readableBytes() >= 12 && + (buf.getShort(buf.readerIndex() + 2) & 0x8000) != 0; + } + + private static boolean isPrintable(byte b) { + return b >= 32 && b < 127; + } +} \ No newline at end of file diff --git a/proxy-server/webpages/lanproxy-config/html/client/list.html b/proxy-server/webpages/lanproxy-config/html/client/list.html index d4c035a7..36e13800 100644 --- a/proxy-server/webpages/lanproxy-config/html/client/list.html +++ b/proxy-server/webpages/lanproxy-config/html/client/list.html @@ -49,6 +49,7 @@ <%:=$.i18n.prop('client.name')%> <%:=$.i18n.prop('client.key')%> <%:=$.i18n.prop('client.status')%> + 映射数量 <%:=$.i18n.prop('public.options')%> @@ -64,6 +65,7 @@ <%:=$.i18n.prop('client.status.offline')%> <% }%> + <%:=data[i].proxyMappings.length%> <%:=$.i18n.prop('public.edit')%> <%:=$.i18n.prop('public.delete')%> diff --git a/proxy-server/webpages/lanproxy-config/html/request_logs.html b/proxy-server/webpages/lanproxy-config/html/request_logs.html new file mode 100644 index 00000000..156ee621 --- /dev/null +++ b/proxy-server/webpages/lanproxy-config/html/request_logs.html @@ -0,0 +1,60 @@ +
+
    +
  • 请求日志
  • +
+
+
+
+
+ + \ No newline at end of file diff --git a/proxy-server/webpages/lanproxy-config/html/statistics/list.html b/proxy-server/webpages/lanproxy-config/html/statistics/list.html index fbb857fa..0e5be56c 100644 --- a/proxy-server/webpages/lanproxy-config/html/statistics/list.html +++ b/proxy-server/webpages/lanproxy-config/html/statistics/list.html @@ -2,29 +2,11 @@
-
+
+
+
\ No newline at end of file diff --git a/proxy-server/webpages/lanproxy-config/index.html b/proxy-server/webpages/lanproxy-config/index.html index 93881a87..fdca82f4 100644 --- a/proxy-server/webpages/lanproxy-config/index.html +++ b/proxy-server/webpages/lanproxy-config/index.html @@ -5,7 +5,9 @@ - + + +