作者 钟来

开发透传服务

正在显示 29 个修改的文件 包含 995 行增加0 行删除
@@ -76,6 +76,10 @@ @@ -76,6 +76,10 @@
76 <groupId>com.zhonglai</groupId> 76 <groupId>com.zhonglai</groupId>
77 <artifactId>weixin-api</artifactId> 77 <artifactId>weixin-api</artifactId>
78 </dependency> 78 </dependency>
  79 + <dependency>
  80 + <groupId>org.apache.xmlgraphics</groupId>
  81 + <artifactId>batik-transcoder</artifactId>
  82 + </dependency>
79 </dependencies> 83 </dependencies>
80 84
81 <build> 85 <build>
@@ -9,11 +9,21 @@ import com.ruoyi.common.utils.GsonConstructor; @@ -9,11 +9,21 @@ import com.ruoyi.common.utils.GsonConstructor;
9 import com.zhonglai.luhui.api.controller.test.dto.ClueData; 9 import com.zhonglai.luhui.api.controller.test.dto.ClueData;
10 import io.swagger.annotations.Api; 10 import io.swagger.annotations.Api;
11 import io.swagger.annotations.ApiOperation; 11 import io.swagger.annotations.ApiOperation;
  12 +import org.apache.batik.transcoder.Transcoder;
  13 +import org.apache.batik.transcoder.TranscoderException;
  14 +import org.apache.batik.transcoder.TranscoderInput;
  15 +import org.apache.batik.transcoder.TranscoderOutput;
  16 +import org.apache.batik.transcoder.image.JPEGTranscoder;
  17 +import org.apache.batik.transcoder.image.PNGTranscoder;
12 import org.springframework.util.StreamUtils; 18 import org.springframework.util.StreamUtils;
13 import org.springframework.web.bind.annotation.*; 19 import org.springframework.web.bind.annotation.*;
14 20
  21 +import javax.servlet.ServletOutputStream;
15 import javax.servlet.http.HttpServletRequest; 22 import javax.servlet.http.HttpServletRequest;
  23 +import javax.servlet.http.HttpServletResponse;
16 import java.io.IOException; 24 import java.io.IOException;
  25 +import java.io.OutputStreamWriter;
  26 +import java.io.StringReader;
17 import java.util.Enumeration; 27 import java.util.Enumeration;
18 import java.util.HashMap; 28 import java.util.HashMap;
19 import java.util.Map; 29 import java.util.Map;
@@ -90,6 +100,65 @@ public class TestController { @@ -90,6 +100,65 @@ public class TestController {
90 "}"; 100 "}";
91 } 101 }
92 102
  103 + @ApiOperation("重写Highcharts导出")
  104 + @RequestMapping(value = "getFeishuTable")
  105 + public void getFeishuTable(HttpServletRequest request, HttpServletResponse response) throws IOException {
  106 + String type = request.getParameter("type");
  107 + String svg = request.getParameter("svg");
  108 + String filename = request.getParameter("filename");
  109 + filename = filename == null ? "chart" : filename;
  110 +
  111 + response.setCharacterEncoding("utf-8");
  112 + response.addHeader("Content-Disposition", "attachment; filename=" + filename + "." + getFileExtension(type));
  113 + response.addHeader("Content-Type", type);
  114 +
  115 + ServletOutputStream out = response.getOutputStream();
  116 +
  117 + try {
  118 + if (type != null && svg != null) {
  119 + svg = svg.replaceAll(":rect", "rect");
  120 +
  121 + Transcoder t = null;
  122 + if ("image/png".equals(type)) {
  123 + t = new PNGTranscoder();
  124 + } else if ("image/jpeg".equals(type)) {
  125 + t = new JPEGTranscoder();
  126 + }
  127 +
  128 + if (t != null) {
  129 + TranscoderInput input = new TranscoderInput(new StringReader(svg));
  130 + TranscoderOutput output = new TranscoderOutput(out);
  131 + t.transcode(input, output);
  132 + } else if ("image/svg+xml".equals(type)) {
  133 + OutputStreamWriter writer = new OutputStreamWriter(out, "UTF-8");
  134 + writer.write(svg);
  135 + writer.flush();
  136 + writer.close();
  137 + } else {
  138 + out.write("Invalid type: ".getBytes());
  139 + }
  140 + } else {
  141 + response.setContentType("text/html");
  142 + out.write("Usage:\n\tParameter [svg]: The DOM Element to be converted.\n\tParameter [type]: The destination MIME type for the element to be transcoded.".getBytes());
  143 + }
  144 + } catch (TranscoderException e) {
  145 + response.reset();
  146 + response.setContentType("text/plain");
  147 + out.write("Problem transcoding stream. See server logs for details.".getBytes());
  148 + e.printStackTrace();
  149 + } finally {
  150 + out.flush();
  151 + out.close();
  152 + }
  153 + }
  154 +
  155 + private String getFileExtension(String mimeType) {
  156 + if ("image/png".equals(mimeType)) return "png";
  157 + if ("image/jpeg".equals(mimeType)) return "jpg";
  158 + if ("image/svg+xml".equals(mimeType)) return "svg";
  159 + return "bin";
  160 + }
  161 +
93 public static void main(String[] args) { 162 public static void main(String[] args) {
94 JSONArray sort = new JSONArray(); 163 JSONArray sort = new JSONArray();
95 JSONObject field_name = new JSONObject(); 164 JSONObject field_name = new JSONObject();
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project xmlns="http://maven.apache.org/POM/4.0.0"
  3 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5 + <modelVersion>4.0.0</modelVersion>
  6 + <parent>
  7 + <groupId>com.zhonglai.luhui</groupId>
  8 + <artifactId>lh-neutrino-proxy</artifactId>
  9 + <version>1.0-SNAPSHOT</version>
  10 + </parent>
  11 +
  12 + <artifactId>lh-neutrino-proxy-client</artifactId>
  13 +
  14 + <properties>
  15 + <maven.compiler.source>8</maven.compiler.source>
  16 + <maven.compiler.target>8</maven.compiler.target>
  17 + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  18 + </properties>
  19 +
  20 +
  21 + <dependencies>
  22 + <dependency>
  23 + <groupId>com.zhonglai.luhui</groupId>
  24 + <artifactId>lh-neutrino-proxy-common</artifactId>
  25 + </dependency>
  26 + </dependencies>
  27 +
  28 +</project>
  1 +package com.zhonglai.luhui.neutrino.proxy.client;
  2 +
  3 +import com.fasterxml.jackson.databind.ObjectMapper;
  4 +import com.zhonglai.luhui.neutrino.proxy.common.RegisterMessage;
  5 +
  6 +import java.io.BufferedReader;
  7 +import java.io.InputStreamReader;
  8 +import java.io.PrintWriter;
  9 +import java.net.Socket;
  10 +import java.util.HashMap;
  11 +import java.util.Map;
  12 +
  13 +public class ClientMain {
  14 + public static void main(String[] args) throws Exception {
  15 + Socket socket = new Socket("127.0.0.1", 9000);
  16 + PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
  17 + BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
  18 +
  19 + ObjectMapper mapper = new ObjectMapper();
  20 + RegisterMessage register = new RegisterMessage("clientA", "abc123");
  21 +
  22 + writer.println(mapper.writeValueAsString(register));
  23 +
  24 + String response = reader.readLine();
  25 + System.out.println("服务端响应:" + response);
  26 + new Thread(new HeartbeatTask(socket)).start();
  27 + }
  28 +}
  1 +package com.zhonglai.luhui.neutrino.proxy.client;
  2 +
  3 +import com.fasterxml.jackson.databind.ObjectMapper;
  4 +
  5 +import java.io.PrintWriter;
  6 +import java.net.Socket;
  7 +import java.util.HashMap;
  8 +import java.util.Map;
  9 +
  10 +/**
  11 + * 心跳线程(每30秒发送)
  12 + */
  13 +public class HeartbeatTask implements Runnable {
  14 + private final Socket socket;
  15 +
  16 + public HeartbeatTask(Socket socket) {
  17 + this.socket = socket;
  18 + }
  19 +
  20 + @Override
  21 + public void run() {
  22 + try {
  23 + PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
  24 + ObjectMapper mapper = new ObjectMapper();
  25 + while (!socket.isClosed()) {
  26 + Map<String, String> heartbeat = new HashMap<>();
  27 + heartbeat.put("type", "heartbeat");
  28 + heartbeat.put("clientId", "clientA");
  29 + writer.println(mapper.writeValueAsString(heartbeat));
  30 + Thread.sleep(30000); // 30秒心跳
  31 + }
  32 + } catch (Exception e) {
  33 + System.out.println("心跳发送失败:" + e.getMessage());
  34 + }
  35 + }
  36 +}
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project xmlns="http://maven.apache.org/POM/4.0.0"
  3 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5 + <modelVersion>4.0.0</modelVersion>
  6 + <parent>
  7 + <groupId>com.zhonglai.luhui</groupId>
  8 + <artifactId>lh-neutrino-proxy</artifactId>
  9 + <version>1.0-SNAPSHOT</version>
  10 + </parent>
  11 +
  12 + <artifactId>lh-neutrino-proxy-common</artifactId>
  13 +
  14 + <properties>
  15 + <maven.compiler.source>8</maven.compiler.source>
  16 + <maven.compiler.target>8</maven.compiler.target>
  17 + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  18 + </properties>
  19 +
  20 + <dependencies>
  21 + <dependency>
  22 + <groupId>com.fasterxml.jackson.core</groupId>
  23 + <artifactId>jackson-databind</artifactId>
  24 + </dependency>
  25 + </dependencies>
  26 +
  27 +</project>
  1 +package com.zhonglai.luhui.neutrino.proxy.common;
  2 +
  3 +/**
  4 + * 注册协议类
  5 + */
  6 +public class RegisterMessage {
  7 + public String type = "register";
  8 + public String clientId;
  9 + public String token;
  10 +
  11 + public RegisterMessage() {}
  12 +
  13 + public RegisterMessage(String clientId, String token) {
  14 + this.clientId = clientId;
  15 + this.token = token;
  16 + }
  17 +}
  1 +package com.zhonglai.luhui.neutrino.proxy.common;
  2 +
  3 +/**
  4 + * 隧道请求协议
  5 + */
  6 +public class TunnelMessage {
  7 + public String type = "tunnel";
  8 + public String tunnelId; // UUID
  9 + public String clientId;
  10 + public int remotePort; // 公网端口
  11 + public String targetHost;
  12 + public int targetPort;
  13 +
  14 +}
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project xmlns="http://maven.apache.org/POM/4.0.0"
  3 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5 + <modelVersion>4.0.0</modelVersion>
  6 + <parent>
  7 + <groupId>com.zhonglai.luhui</groupId>
  8 + <artifactId>lh-neutrino-proxy</artifactId>
  9 + <version>1.0-SNAPSHOT</version>
  10 + </parent>
  11 +
  12 + <artifactId>lh-neutrino-proxy-server</artifactId>
  13 +
  14 + <properties>
  15 + <maven.compiler.source>8</maven.compiler.source>
  16 + <maven.compiler.target>8</maven.compiler.target>
  17 + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  18 + </properties>
  19 +
  20 + <dependencies>
  21 + <dependency>
  22 + <groupId>io.netty</groupId>
  23 + <artifactId>netty-all</artifactId>
  24 + </dependency>
  25 + <dependency>
  26 + <groupId>com.zhonglai.luhui</groupId>
  27 + <artifactId>lh-neutrino-proxy-common</artifactId>
  28 + </dependency>
  29 + <dependency>
  30 + <groupId>commons-io</groupId>
  31 + <artifactId>commons-io</artifactId>
  32 + </dependency>
  33 + <dependency>
  34 + <groupId>com.alibaba</groupId>
  35 + <artifactId>fastjson</artifactId>
  36 + </dependency>
  37 + </dependencies>
  38 +
  39 +</project>
  1 +package com.zhonglai.luhui.neutrino.proxy.server;
  2 +
  3 +import io.netty.channel.Channel;
  4 +
  5 +import java.util.Map;
  6 +import java.util.concurrent.ConcurrentHashMap;
  7 +
  8 +/**
  9 + * 服务端维护客户端连接
  10 + */
  11 +public class ClientSessionManager {
  12 + private static final Map<String, Channel> clientMap = new ConcurrentHashMap<>();
  13 +
  14 + public static void register(String clientId, Channel channel) {
  15 + clientMap.put(clientId, channel);
  16 + System.out.println("注册客户端:" + clientId);
  17 + }
  18 +
  19 + public static void remove(Channel channel) {
  20 + clientMap.entrySet().removeIf(entry -> entry.getValue().equals(channel));
  21 + }
  22 +
  23 + public static void heartbeat(String clientId) {
  24 + // 更新最后心跳时间(后续可扩展)
  25 + System.out.println("收到心跳:" + clientId);
  26 + }
  27 +
  28 + public static boolean isClientConnected(String clientId) {
  29 + return clientMap.containsKey(clientId);
  30 + }
  31 +
  32 + public static Channel getChannel(String clientId) {
  33 + return clientMap.get(clientId);
  34 + }
  35 +}
  1 +package com.zhonglai.luhui.neutrino.proxy.server;
  2 +
  3 +import com.zhonglai.luhui.neutrino.proxy.server.httpservice.SimpleHttpServer;
  4 +import com.zhonglai.luhui.neutrino.proxy.server.proxy.ProxyServer;
  5 +
  6 +public class ControlServer {
  7 + public static void main(String[] args) throws Exception {
  8 + //启动代理服务
  9 + ProxyServer.start(9000);
  10 +
  11 + //启动接口服务
  12 + SimpleHttpServer.startHttpServer(8080);
  13 + }
  14 +}
  1 +package com.zhonglai.luhui.neutrino.proxy.server.function;
  2 +
  3 +import com.fasterxml.jackson.databind.ObjectMapper;
  4 +import com.fasterxml.jackson.databind.node.ObjectNode;
  5 +import com.zhonglai.luhui.neutrino.proxy.common.TunnelMessage;
  6 +import com.zhonglai.luhui.neutrino.proxy.server.ClientSessionManager;
  7 +import io.netty.channel.Channel;
  8 +
  9 +import java.io.*;
  10 +import java.net.*;
  11 +import java.util.Map;
  12 +import java.util.UUID;
  13 +import java.util.concurrent.*;
  14 +
  15 +public class PortForwardManager {
  16 + public static final Map<Integer, ServerSocket> activeServers = new ConcurrentHashMap<>();
  17 + public static final Map<String, Integer> clientBandwidthMap = new ConcurrentHashMap<>();
  18 +
  19 + public void startPortForward(int remotePort, String clientId, int targetPort) throws IOException {
  20 + if (activeServers.containsKey(remotePort)) return;
  21 +
  22 + ServerSocket serverSocket = new ServerSocket(remotePort);
  23 + activeServers.put(remotePort, serverSocket);
  24 +
  25 + new Thread(() -> {
  26 + System.out.println("端口转发监听启动: " + remotePort + " => " + clientId + ":" + targetPort);
  27 + while (true) {
  28 + try {
  29 + Socket externalSocket = serverSocket.accept(); // 有外部连接过来了
  30 + String tunnelId = UUID.randomUUID().toString();
  31 +
  32 + // 1. 注册外部连接 socket(暂时只 createBridge)
  33 + TunnelBridgePool.createBridge(tunnelId, externalSocket);
  34 +
  35 + // 2. 通过 control 通道通知客户端建立内网连接
  36 + Channel channel = ClientSessionManager.getChannel(clientId);
  37 + if (channel != null) {
  38 + ObjectMapper mapper = new ObjectMapper();
  39 + ObjectNode msg = mapper.createObjectNode();
  40 + msg.put("type", "create_tunnel");
  41 + msg.put("tunnelId", tunnelId);
  42 + msg.put("targetPort", targetPort);
  43 + channel.writeAndFlush(msg.toString() + "\n");
  44 + } else {
  45 + System.err.println("客户端未连接,无法建立隧道: " + clientId);
  46 + externalSocket.close();
  47 + }
  48 +
  49 + // ✅ 注意:此时 **不要 bindAndStart**,等客户端连接进来后才执行。
  50 +
  51 + } catch (IOException e) {
  52 + e.printStackTrace();
  53 + }
  54 + }
  55 + }).start();
  56 + }
  57 +}
  1 +package com.zhonglai.luhui.neutrino.proxy.server.function;
  2 +
  3 +import com.zhonglai.luhui.neutrino.proxy.server.function.ratelimit.RateLimitedInputStream;
  4 +import com.zhonglai.luhui.neutrino.proxy.server.function.ratelimit.RateLimitedOutputStream;
  5 +import com.zhonglai.luhui.neutrino.proxy.server.function.ratelimit.TokenBucket;
  6 +
  7 +import java.io.IOException;
  8 +import java.io.InputStream;
  9 +import java.io.OutputStream;
  10 +import java.net.Socket;
  11 +import java.util.Map;
  12 +import java.util.concurrent.ConcurrentHashMap;
  13 +
  14 +/**
  15 + * 服务端建立转发通道并绑定 tunnelId
  16 + */
  17 +public class TunnelBridgePool {
  18 + private static final Map<String, Socket> bridgeMap = new ConcurrentHashMap<>();
  19 + private static final Map<String, Integer> bandwidthMap = new ConcurrentHashMap<>();
  20 +
  21 + public static void createBridge(String tunnelId, Socket externalSocket) {
  22 + bridgeMap.put(tunnelId, externalSocket);
  23 + }
  24 +
  25 + public static void bindAndStart(String tunnelId, Socket tunnelSocket, int rate) {
  26 + bandwidthMap.put(tunnelId, rate);
  27 + bindAndStart(tunnelId, tunnelSocket);
  28 + }
  29 +
  30 + public static void bindAndStart(String tunnelId, Socket tunnelSocket) {
  31 + Socket externalSocket = bridgeMap.remove(tunnelId);
  32 + int rate = bandwidthMap.remove(tunnelId);
  33 + if (externalSocket != null) {
  34 + try {
  35 + TokenBucket upload = new TokenBucket(rate, rate);
  36 + TokenBucket download = new TokenBucket(rate, rate);
  37 +
  38 + InputStream fromClient = new RateLimitedInputStream(tunnelSocket.getInputStream(), upload);
  39 + OutputStream toExternal = new RateLimitedOutputStream(externalSocket.getOutputStream(), upload);
  40 +
  41 + InputStream fromExternal = new RateLimitedInputStream(externalSocket.getInputStream(), download);
  42 + OutputStream toClient = new RateLimitedOutputStream(tunnelSocket.getOutputStream(), download);
  43 +
  44 + new Thread(() -> copy(fromClient, toExternal)).start();
  45 + new Thread(() -> copy(fromExternal, toClient)).start();
  46 + } catch (IOException e) {
  47 + e.printStackTrace();
  48 + }
  49 + }
  50 + }
  51 +
  52 + private static void copy(InputStream in, OutputStream out) {
  53 + try {
  54 + byte[] buffer = new byte[8192];
  55 + int len;
  56 + while ((len = in.read(buffer)) != -1) {
  57 + out.write(buffer, 0, len);
  58 + out.flush();
  59 + }
  60 + } catch (IOException ignored) {
  61 + }
  62 + }
  63 +}
  1 +package com.zhonglai.luhui.neutrino.proxy.server.function;
  2 +
  3 +import java.io.BufferedReader;
  4 +import java.io.InputStreamReader;
  5 +import java.net.ServerSocket;
  6 +import java.net.Socket;
  7 +
  8 +/**
  9 + * 服务端独立启动数据转发端口(如 9100)
  10 + */
  11 +public class TunnelSocketListener {
  12 + public void start() throws Exception {
  13 + ServerSocket serverSocket = new ServerSocket(9100);
  14 + System.out.println("数据转发端口监听中: 9100");
  15 + while (true) {
  16 + Socket socket = serverSocket.accept();
  17 + BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
  18 + String tunnelId = reader.readLine();
  19 + TunnelBridgePool.bindAndStart(tunnelId, socket);
  20 + }
  21 + }
  22 +}
  1 +package com.zhonglai.luhui.neutrino.proxy.server.function.ratelimit;
  2 +
  3 +import java.io.FilterInputStream;
  4 +import java.io.IOException;
  5 +import java.io.InputStream;
  6 +
  7 +/**
  8 + * 限速 InputStream 和 OutputStream 封装类
  9 + */
  10 +public class RateLimitedInputStream extends FilterInputStream {
  11 + private final TokenBucket bucket;
  12 +
  13 + public RateLimitedInputStream(InputStream in, TokenBucket bucket) {
  14 + super(in);
  15 + this.bucket = bucket;
  16 + }
  17 +
  18 + @Override
  19 + public int read(byte[] b, int off, int len) throws IOException {
  20 + try {
  21 + bucket.consume(len);
  22 + } catch (InterruptedException e) {
  23 + Thread.currentThread().interrupt();
  24 + return -1;
  25 + }
  26 + return super.read(b, off, len);
  27 + }
  28 +}
  1 +package com.zhonglai.luhui.neutrino.proxy.server.function.ratelimit;
  2 +
  3 +import java.io.FilterOutputStream;
  4 +import java.io.IOException;
  5 +import java.io.OutputStream;
  6 +
  7 +public class RateLimitedOutputStream extends FilterOutputStream {
  8 + private final TokenBucket bucket;
  9 +
  10 + public RateLimitedOutputStream(OutputStream out, TokenBucket bucket) {
  11 + super(out);
  12 + this.bucket = bucket;
  13 + }
  14 +
  15 + @Override
  16 + public void write(byte[] b, int off, int len) throws IOException {
  17 + try {
  18 + bucket.consume(len);
  19 + } catch (InterruptedException e) {
  20 + Thread.currentThread().interrupt();
  21 + return;
  22 + }
  23 + super.write(b, off, len);
  24 + }
  25 +}
  1 +package com.zhonglai.luhui.neutrino.proxy.server.function.ratelimit;
  2 +
  3 +/**
  4 + * 核心限速器
  5 + */
  6 +public class TokenBucket {
  7 + private final long capacity;
  8 + private final long refillRate;
  9 + private long tokens;
  10 + private long lastRefillTime;
  11 +
  12 + public TokenBucket(long capacity, long refillRate) {
  13 + this.capacity = capacity;
  14 + this.refillRate = refillRate;
  15 + this.tokens = capacity;
  16 + this.lastRefillTime = System.nanoTime();
  17 + }
  18 +
  19 + public synchronized void consume(long bytes) throws InterruptedException {
  20 + refill();
  21 + while (tokens < bytes) {
  22 + Thread.sleep(5);
  23 + refill();
  24 + }
  25 + tokens -= bytes;
  26 + }
  27 +
  28 + private void refill() {
  29 + long now = System.nanoTime();
  30 + long elapsed = now - lastRefillTime;
  31 + long refillTokens = (elapsed * refillRate) / 1_000_000_000L;
  32 + if (refillTokens > 0) {
  33 + tokens = Math.min(capacity, tokens + refillTokens);
  34 + lastRefillTime = now;
  35 + }
  36 + }
  37 +}
  1 +package com.zhonglai.luhui.neutrino.proxy.server.httpservice;
  2 +
  3 +import com.alibaba.fastjson.JSON;
  4 +import com.alibaba.fastjson.JSONObject;
  5 +import com.sun.net.httpserver.HttpExchange;
  6 +import com.sun.net.httpserver.HttpHandler;
  7 +import com.zhonglai.luhui.neutrino.proxy.server.function.PortForwardManager;
  8 +import com.zhonglai.luhui.neutrino.proxy.server.httpservice.dto.ResponseMessage;
  9 +
  10 +import java.io.*;
  11 +import java.net.URLDecoder;
  12 +import java.nio.charset.StandardCharsets;
  13 +import java.util.HashMap;
  14 +import java.util.Map;
  15 +
  16 +/**
  17 + * 端口映射操作
  18 + */
  19 +public class PortForwardHandler implements HttpHandler {
  20 + @Override
  21 + public void handle(HttpExchange httpExchange) throws IOException {
  22 + try {
  23 + Map<String,String> params = getParamsMap(httpExchange);
  24 + String operate = params.get("operate");
  25 + switch (operate)
  26 + {
  27 + case "map": //端口映射
  28 + map(httpExchange,params);
  29 + return;
  30 + case "updateBandwidth": //限制带宽
  31 + updateBandwidth(httpExchange,params);
  32 + return;
  33 + default:
  34 + response(httpExchange,new ResponseMessage("不支持的操作", 0));
  35 + }
  36 + }catch (Exception e)
  37 + {
  38 + response(httpExchange,new ResponseMessage("接口请求失败", 0));
  39 + }
  40 + }
  41 +
  42 + private void map(HttpExchange httpExchange, Map<String, String> params)throws IOException
  43 + {
  44 +
  45 + int serverPort = Integer.parseInt(params.get("serverPort"));
  46 + String clientId =params.get("clientId");
  47 + int targetPort = Integer.parseInt(params.get("targetPort"));
  48 + int bandwidth = null != params.get("bandwidth") ? Integer.parseInt(params.get("bandwidth")) : 100 * 1024;
  49 + PortForwardManager.clientBandwidthMap.put(clientId, bandwidth);
  50 +
  51 + new PortForwardManager().startPortForward(serverPort, clientId, targetPort);
  52 + response(httpExchange,new ResponseMessage("映射成功", 1));
  53 + }
  54 +
  55 + private void updateBandwidth(HttpExchange httpExchange, Map<String, String> params)throws IOException
  56 + {
  57 + String clientId = params.get("clientId");
  58 + int bandwidth = Integer.parseInt(params.get("bandwidth"));
  59 + PortForwardManager.clientBandwidthMap.put(clientId, bandwidth);
  60 + response(httpExchange,new ResponseMessage("带宽更新成功", 1));
  61 + }
  62 +
  63 + private void response(HttpExchange httpExchange, ResponseMessage responseMessage) throws IOException
  64 + {
  65 + String response = JSON.toJSONString(responseMessage);
  66 + OutputStream os = httpExchange.getResponseBody();
  67 + httpExchange.getResponseHeaders().set("Content-Type", "application/json; charset=UTF-8");
  68 + byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
  69 + httpExchange.sendResponseHeaders(200, bytes.length);
  70 + os.write(bytes);
  71 + os.close();
  72 + }
  73 +
  74 + private Map<String, String> getQueryParams(HttpExchange exchange) throws UnsupportedEncodingException {
  75 + String query = exchange.getRequestURI().getQuery(); // 获取 name=张三&age=18
  76 + return parseQuery(query);
  77 + }
  78 +
  79 + private Map<String, String> parseQuery(String query) throws UnsupportedEncodingException {
  80 + Map<String, String> result = new HashMap<>();
  81 + if (query == null || query.isEmpty()) return result;
  82 +
  83 + for (String param : query.split("&")) {
  84 + String[] pair = param.split("=", 2);
  85 + if (pair.length == 2) {
  86 + result.put(URLDecoder.decode(pair[0], StandardCharsets.UTF_8.toString()),URLDecoder.decode(pair[1], StandardCharsets.UTF_8.toString()));
  87 + } else if (pair.length == 1) {
  88 + result.put(URLDecoder.decode(pair[0], StandardCharsets.UTF_8.toString()), "");
  89 + }
  90 + }
  91 + return result;
  92 + }
  93 +
  94 + private Map<String, String> getParamsMap(HttpExchange httpExchange) throws IOException {
  95 + String method = httpExchange.getRequestMethod();
  96 + Map<String, String> params = null;
  97 + if ("POST".equalsIgnoreCase(method))
  98 + {
  99 + params = getPostParams(httpExchange);
  100 + } else if ("GET".equalsIgnoreCase(method))
  101 + {
  102 + params = getQueryParams(httpExchange);
  103 + }
  104 + return params;
  105 + }
  106 +
  107 + private Map<String, String> getPostParams(HttpExchange exchange) throws IOException {
  108 + String body = readRequestBody(exchange);
  109 + return parseQuery(body); // 和 GET 参数一样解析
  110 + }
  111 +
  112 + private String readRequestBody(HttpExchange exchange) throws IOException {
  113 + BufferedReader reader = new BufferedReader(
  114 + new InputStreamReader(exchange.getRequestBody(), StandardCharsets.UTF_8));
  115 +
  116 + StringBuilder sb = new StringBuilder();
  117 + String line;
  118 + while ((line = reader.readLine()) != null) {
  119 + sb.append(line);
  120 + }
  121 + return sb.toString();
  122 + }
  123 +
  124 +}
  1 +package com.zhonglai.luhui.neutrino.proxy.server.httpservice;
  2 +
  3 +import com.sun.net.httpserver.HttpServer;
  4 +
  5 +import java.io.IOException;
  6 +import java.net.InetSocketAddress;
  7 +
  8 +public class SimpleHttpServer {
  9 + public static void startHttpServer(int port) throws IOException
  10 + {
  11 + HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
  12 +
  13 + // 映射 /static/ 到 static/ 目录
  14 + server.createContext("/static/", new StaticFileHandler("static"));
  15 +
  16 + server.createContext("/portForward", new PortForwardHandler());
  17 +
  18 + server.setExecutor(null); // 默认线程池
  19 + server.start();
  20 + System.out.println("Server started at http://localhost:" + port);
  21 + }
  22 +}
  1 +package com.zhonglai.luhui.neutrino.proxy.server.httpservice;
  2 +
  3 +import com.sun.net.httpserver.HttpExchange;
  4 +import com.sun.net.httpserver.HttpHandler;
  5 +
  6 +import java.io.IOException;
  7 +import java.io.OutputStream;
  8 +import java.nio.file.Files;
  9 +import java.nio.file.Path;
  10 +import java.nio.file.Paths;
  11 +
  12 +/**
  13 + * 静态文件处理器
  14 + */
  15 +public class StaticFileHandler implements HttpHandler {
  16 + private final Path basePath;
  17 +
  18 + public StaticFileHandler(String rootDir) {
  19 + this.basePath = Paths.get(rootDir).toAbsolutePath();
  20 + }
  21 +
  22 + @Override
  23 + public void handle(HttpExchange exchange) throws IOException {
  24 + String uriPath = exchange.getRequestURI().getPath();
  25 + String other = uriPath.replaceFirst("^/[^/]+/", "");
  26 + Path filePath = basePath.resolve(other).normalize();
  27 + //生成
  28 + if (!filePath.startsWith(basePath) || !Files.exists(filePath)) {
  29 + exchange.sendResponseHeaders(404, -1);
  30 + return;
  31 + }
  32 +
  33 + String contentType = guessContentType(filePath);
  34 + byte[] fileBytes = Files.readAllBytes(filePath);
  35 + exchange.getResponseHeaders().add("Content-Type", contentType);
  36 + exchange.sendResponseHeaders(200, fileBytes.length);
  37 + try (OutputStream os = exchange.getResponseBody()) {
  38 + os.write(fileBytes);
  39 + }
  40 + }
  41 +
  42 + private String guessContentType(Path path) {
  43 + String name = path.getFileName().toString().toLowerCase();
  44 + if (name.endsWith(".html")) return "text/html";
  45 + if (name.endsWith(".js")) return "application/javascript";
  46 + if (name.endsWith(".css")) return "text/css";
  47 + if (name.endsWith(".m3u8")) return "application/vnd.apple.mpegurl";
  48 + if (name.endsWith(".ts")) return "video/MP2T";
  49 + return "application/octet-stream";
  50 + }
  51 +}
  1 +package com.zhonglai.luhui.neutrino.proxy.server.httpservice.dto;
  2 +
  3 +public class MappingPort {
  4 + private Integer clientPort; //客服端端口
  5 + private Integer servicePort; //服务器端口
  6 +
  7 + public Integer getClientPort() {
  8 + return clientPort;
  9 + }
  10 +
  11 + public void setClientPort(Integer clientPort) {
  12 + this.clientPort = clientPort;
  13 + }
  14 +
  15 + public Integer getServicePort() {
  16 + return servicePort;
  17 + }
  18 +
  19 + public void setServicePort(Integer servicePort) {
  20 + this.servicePort = servicePort;
  21 + }
  22 +}
  1 +package com.zhonglai.luhui.neutrino.proxy.server.httpservice.dto;
  2 +
  3 +import java.util.List;
  4 +import java.util.Map;
  5 +
  6 +/**
  7 + * 端口映射内容
  8 + */
  9 +public class PortForwardMapBody {
  10 + private String clientId;
  11 + private List<MappingPort> mappingPortList;
  12 +
  13 + public String getClientId() {
  14 + return clientId;
  15 + }
  16 +
  17 + public void setClientId(String clientId) {
  18 + this.clientId = clientId;
  19 + }
  20 +
  21 + public List<MappingPort> getMappingPortList() {
  22 + return mappingPortList;
  23 + }
  24 +
  25 + public void setMappingPortList(List<MappingPort> mappingPortList) {
  26 + this.mappingPortList = mappingPortList;
  27 + }
  28 +}
  1 +package com.zhonglai.luhui.neutrino.proxy.server.httpservice.dto;
  2 +
  3 +public class ResponseMessage {
  4 + private String msg;
  5 + private Integer code;
  6 + private Object data;
  7 +
  8 + public ResponseMessage(String msg, Integer code) {
  9 + this.msg = msg;
  10 + this.code = code;
  11 + }
  12 +
  13 + public ResponseMessage(String msg, Integer code, Object data) {
  14 + this.msg = msg;
  15 + this.code = code;
  16 + this.data = data;
  17 + }
  18 +
  19 + public String getMsg() {
  20 + return msg;
  21 + }
  22 +
  23 + public void setMsg(String msg) {
  24 + this.msg = msg;
  25 + }
  26 +
  27 + public Integer getCode() {
  28 + return code;
  29 + }
  30 +
  31 + public void setCode(Integer code) {
  32 + this.code = code;
  33 + }
  34 +
  35 + public Object getData() {
  36 + return data;
  37 + }
  38 +
  39 + public void setData(Object data) {
  40 + this.data = data;
  41 + }
  42 +}
  1 +package com.zhonglai.luhui.neutrino.proxy.server.proxy;
  2 +
  3 +import com.zhonglai.luhui.neutrino.proxy.server.proxy.handler.ControlHandler;
  4 +import com.zhonglai.luhui.neutrino.proxy.server.proxy.handler.HeartbeatTimeoutHandler;
  5 +import io.netty.bootstrap.ServerBootstrap;
  6 +import io.netty.channel.ChannelFuture;
  7 +import io.netty.channel.ChannelFutureListener;
  8 +import io.netty.channel.ChannelInitializer;
  9 +import io.netty.channel.nio.NioEventLoopGroup;
  10 +import io.netty.channel.socket.SocketChannel;
  11 +import io.netty.channel.socket.nio.NioServerSocketChannel;
  12 +import io.netty.handler.codec.LineBasedFrameDecoder;
  13 +import io.netty.handler.codec.string.StringDecoder;
  14 +import io.netty.handler.codec.string.StringEncoder;
  15 +import io.netty.handler.timeout.IdleStateHandler;
  16 +
  17 +/**
  18 + * 代理服务
  19 + */
  20 +public class ProxyServer {
  21 + public static void start(int proxyPort) throws Exception
  22 + {
  23 + NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
  24 + NioEventLoopGroup workerGroup = new NioEventLoopGroup();
  25 +
  26 + ServerBootstrap bootstrap = new ServerBootstrap()
  27 + .group(bossGroup, workerGroup)
  28 + .channel(NioServerSocketChannel.class)
  29 + .childHandler(new ChannelInitializer<SocketChannel>() {
  30 + @Override
  31 + protected void initChannel(SocketChannel ch) {
  32 + ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
  33 + ch.pipeline().addLast(new IdleStateHandler(60, 0, 0));
  34 + ch.pipeline().addLast(new HeartbeatTimeoutHandler());
  35 + ch.pipeline().addLast(new StringDecoder());
  36 + ch.pipeline().addLast(new StringEncoder());
  37 + ch.pipeline().addLast(new ControlHandler());
  38 + }
  39 + });
  40 +
  41 + ChannelFuture future = bootstrap.bind(proxyPort).sync(); // bind 完成阻塞等待
  42 +
  43 + future.channel().closeFuture().addListener((ChannelFutureListener) closeFuture -> {
  44 + System.out.println("Server channel closed.");
  45 + });
  46 + System.out.println("Netty server started on port: " + proxyPort);
  47 +
  48 + // ✅ 注册优雅关闭钩子,不阻塞主线程
  49 + Runtime.getRuntime().addShutdownHook(new Thread(() -> {
  50 + System.out.println("Shutdown signal received. Closing Netty gracefully...");
  51 + bossGroup.shutdownGracefully();
  52 + workerGroup.shutdownGracefully();
  53 + }));
  54 +
  55 +
  56 + }
  57 +}
  1 +package com.zhonglai.luhui.neutrino.proxy.server.proxy.handler;
  2 +
  3 +import com.fasterxml.jackson.databind.JsonNode;
  4 +import com.fasterxml.jackson.databind.ObjectMapper;
  5 +import com.zhonglai.luhui.neutrino.proxy.common.RegisterMessage;
  6 +import com.zhonglai.luhui.neutrino.proxy.server.ClientSessionManager;
  7 +import com.zhonglai.luhui.neutrino.proxy.server.function.PortForwardManager;
  8 +import io.netty.channel.ChannelHandlerContext;
  9 +import io.netty.channel.SimpleChannelInboundHandler;
  10 +import org.apache.commons.io.IOUtils;
  11 +
  12 +import java.io.IOException;
  13 +import java.io.PrintWriter;
  14 +import java.net.Socket;
  15 +
  16 +public class ControlHandler extends SimpleChannelInboundHandler<String> {
  17 + private static final ObjectMapper mapper = new ObjectMapper();
  18 + private String clientId;
  19 +
  20 + @Override
  21 + protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
  22 + JsonNode json = mapper.readTree(msg);
  23 + String type = json.get("type").asText();
  24 +
  25 + switch (type) {
  26 + case "register":
  27 + clientId = json.get("clientId").asText();
  28 + ClientSessionManager.register(clientId, ctx.channel());
  29 + ctx.writeAndFlush("{\"type\":\"register_ack\",\"status\":\"ok\"}\n");
  30 + break;
  31 +
  32 + case "heartbeat":
  33 + ClientSessionManager.heartbeat(json.get("clientId").asText());
  34 + break;
  35 + default:
  36 + System.out.println("未知消息类型: " + type);
  37 + }
  38 + }
  39 +
  40 + @Override
  41 + public void channelInactive(ChannelHandlerContext ctx) throws Exception {
  42 + ClientSessionManager.remove(ctx.channel());
  43 + System.out.println("客户端断开连接:" + clientId);
  44 + }
  45 +
  46 +}
  1 +package com.zhonglai.luhui.neutrino.proxy.server.proxy.handler;
  2 +
  3 +import io.netty.channel.ChannelHandlerContext;
  4 +import io.netty.channel.ChannelInboundHandlerAdapter;
  5 +import io.netty.handler.timeout.IdleState;
  6 +import io.netty.handler.timeout.IdleStateEvent;
  7 +
  8 +public class HeartbeatTimeoutHandler extends ChannelInboundHandlerAdapter {
  9 + @Override
  10 + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
  11 + if (evt instanceof IdleStateEvent && ((IdleStateEvent)evt).state() == IdleState.READER_IDLE) {
  12 + System.out.println("心跳超时,断开客户端:" + ctx.channel().remoteAddress());
  13 + ctx.close();
  14 + } else {
  15 + super.userEventTriggered(ctx, evt);
  16 + }
  17 + }
  18 +}
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project xmlns="http://maven.apache.org/POM/4.0.0"
  3 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5 + <modelVersion>4.0.0</modelVersion>
  6 + <parent>
  7 + <groupId>com.zhonglai.luhui</groupId>
  8 + <artifactId>lh-modules</artifactId>
  9 + <version>1.0-SNAPSHOT</version>
  10 + </parent>
  11 +
  12 + <artifactId>lh-neutrino-proxy</artifactId>
  13 + <modules>
  14 + <module>lh-neutrino-proxy-server</module>
  15 + <module>lh-neutrino-proxy-client</module>
  16 + <module>lh-neutrino-proxy-common</module>
  17 + </modules>
  18 +
  19 + <properties>
  20 + <maven.compiler.source>8</maven.compiler.source>
  21 + <maven.compiler.target>8</maven.compiler.target>
  22 + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  23 + </properties>
  24 +
  25 + <packaging>pom</packaging>
  26 + <description>
  27 + 透传代理
  28 + </description>
  29 +</project>
@@ -38,6 +38,7 @@ @@ -38,6 +38,7 @@
38 <module>lh-ssh-service-lesten</module> 38 <module>lh-ssh-service-lesten</module>
39 <module>lh-deviceInfo-sync</module> 39 <module>lh-deviceInfo-sync</module>
40 <module>lh-camera</module> 40 <module>lh-camera</module>
  41 + <module>lh-neutrino-proxy</module>
41 </modules> 42 </modules>
42 43
43 <properties> 44 <properties>
@@ -369,6 +369,11 @@ @@ -369,6 +369,11 @@
369 <version>${ruoyi.version}</version> 369 <version>${ruoyi.version}</version>
370 </dependency> 370 </dependency>
371 <dependency> 371 <dependency>
  372 + <groupId>com.zhonglai.luhui</groupId>
  373 + <artifactId>lh-neutrino-proxy-common</artifactId>
  374 + <version>${ruoyi.version}</version>
  375 + </dependency>
  376 + <dependency>
372 <groupId>com.zhonglai</groupId> 377 <groupId>com.zhonglai</groupId>
373 <artifactId>ServiceDao</artifactId> 378 <artifactId>ServiceDao</artifactId>
374 <version>1.4.3</version> 379 <version>1.4.3</version>
@@ -603,6 +608,13 @@ @@ -603,6 +608,13 @@
603 <artifactId>httpclient5</artifactId> 608 <artifactId>httpclient5</artifactId>
604 <version>5.1.3</version> 609 <version>5.1.3</version>
605 </dependency> 610 </dependency>
  611 +
  612 + <dependency>
  613 + <groupId>org.apache.xmlgraphics</groupId>
  614 + <artifactId>batik-transcoder</artifactId>
  615 + <version>1.18</version>
  616 + </dependency>
  617 +
606 </dependencies> 618 </dependencies>
607 619
608 620