feat: support ws proxy

This commit is contained in:
ReaJason
2026-01-18 23:49:41 +08:00
parent 54be0712d5
commit 4ec1f06362
22 changed files with 577 additions and 281 deletions
@@ -77,6 +77,10 @@ public class MemShellGenerateRequest {
.shellClassBase64(shellToolConfig.getShellClassBase64())
.shellClassName(shellToolConfig.getShellClassName())
.build();
case Proxy -> ProxyConfig.builder()
.headerName(shellToolConfig.getHeaderName())
.headerValue(shellToolConfig.getHeaderValue())
.shellClassName(shellToolConfig.shellClassName).build();
default -> throw new UnsupportedOperationException("unknown shell tool " + shellConfig.getShellTool());
};
}
@@ -9,6 +9,7 @@ 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 com.reajason.javaweb.memshell.shelltool.wsproxy.ProxyWebSocket;
import java.util.Collections;
import java.util.List;
@@ -239,6 +240,13 @@ public class ServerFactory {
.addShellClass(WAS_AGENT_FILTER_MANAGER, NeoreGeorg.class)
.addShellClass(ACTION, NeoreGeorgStruct2Action.class)
.build());
addToolMapping(ShellTool.Proxy, ToolMapping.builder()
.addShellClass(WEBSOCKET, ProxyWebSocket.class)
.addShellClass(JAKARTA_WEBSOCKET, ProxyWebSocket.class)
.addShellClass(BYPASS_NGINX_WEBSOCKET, ProxyWebSocket.class)
.addShellClass(JAKARTA_BYPASS_NGINX_WEBSOCKET, ProxyWebSocket.class)
.build());
}
public static void register(String serverName, Supplier<AbstractServer> shellSupplier) {
@@ -12,5 +12,6 @@ public class ShellTool {
public static final String Suo5v2 = "Suo5v2";
public static final String AntSword = "AntSword";
public static final String NeoreGeorg = "NeoreGeorg";
public static final String Proxy = "Proxy";
public static final String Custom = "Custom";
}
@@ -27,6 +27,7 @@ public class ShellToolFactory {
register(ShellTool.AntSword, AntSwordGenerator.class, AntSwordConfig.class);
register(ShellTool.NeoreGeorg, NeoreGeorgGenerator.class, NeoreGeorgConfig.class);
register(ShellTool.Custom, CustomShellGenerator.class, CustomConfig.class);
register(ShellTool.Proxy, ProxyGenerator.class, ProxyConfig.class);
}
public static void register(String shellToolName, Class<? extends ShellGenerator> generatorClass, Class<? extends ShellToolConfig> configClass) {
@@ -0,0 +1,38 @@
package com.reajason.javaweb.memshell.config;
import com.reajason.javaweb.utils.CommonUtil;
import lombok.*;
import lombok.experimental.SuperBuilder;
import org.apache.commons.lang3.StringUtils;
@Getter
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class ProxyConfig extends ShellToolConfig {
@Builder.Default
private String headerName = "User-Agent";
@Builder.Default
private String headerValue = CommonUtil.getRandomString(8);
public static abstract class ProxyConfigBuilder<C extends ProxyConfig, B extends ProxyConfig.ProxyConfigBuilder<C, B>>
extends ShellToolConfig.ShellToolConfigBuilder<C, B> {
public B headerName(final String headerName) {
if (StringUtils.isNotBlank(headerName)) {
this.headerName$value = headerName;
headerName$set = true;
}
return self();
}
public B headerValue(final String headerValue) {
if (StringUtils.isNotBlank(headerValue)) {
this.headerValue$value = headerValue;
headerValue$set = true;
}
return self();
}
}
}
@@ -0,0 +1,17 @@
package com.reajason.javaweb.memshell.generator;
import com.reajason.javaweb.memshell.config.ProxyConfig;
import com.reajason.javaweb.memshell.config.ShellConfig;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
public class ProxyGenerator extends ByteBuddyShellGenerator<ProxyConfig> {
public ProxyGenerator(ShellConfig shellConfig, ProxyConfig shellToolConfig) {
super(shellConfig, shellToolConfig);
}
@Override
protected DynamicType.Builder<?> getBuilder() {
return new ByteBuddy().redefine(shellToolConfig.getShellClass());
}
}
@@ -5,10 +5,7 @@ import com.reajason.javaweb.GenerationException;
import com.reajason.javaweb.Server;
import com.reajason.javaweb.buddy.ServletRenameVisitorWrapper;
import com.reajason.javaweb.buddy.TargetJreVersionVisitorWrapper;
import com.reajason.javaweb.memshell.config.CommandConfig;
import com.reajason.javaweb.memshell.config.GodzillaConfig;
import com.reajason.javaweb.memshell.config.ShellConfig;
import com.reajason.javaweb.memshell.config.ShellToolConfig;
import com.reajason.javaweb.memshell.config.*;
import com.reajason.javaweb.memshell.shelltool.wsbypass.TomcatWsBypassValve;
import com.reajason.javaweb.utils.CommonUtil;
import net.bytebuddy.ByteBuddy;
@@ -50,6 +47,8 @@ public class WebSocketByPassHelperGenerator {
return Pair.of(((CommandConfig) shellToolConfig).getHeaderName(), ((CommandConfig) shellToolConfig).getHeaderValue());
} else if (shellToolConfig instanceof GodzillaConfig) {
return Pair.of(((GodzillaConfig) shellToolConfig).getHeaderName(), ((GodzillaConfig) shellToolConfig).getHeaderValue());
} else if (shellToolConfig instanceof ProxyConfig) {
return Pair.of(((ProxyConfig) shellToolConfig).getHeaderName(), ((ProxyConfig) shellToolConfig).getHeaderValue());
}
return null;
}
@@ -0,0 +1,16 @@
<%--
Created by IntelliJ IDEA.
User: ReaJason
Date: 2026/1/16
Time: 23:12
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
$END$
</body>
</html>
+109 -98
View File
@@ -1,16 +1,5 @@
import {
ArrowUpRightIcon,
AxeIcon,
CommandIcon,
InfoIcon,
NetworkIcon,
ServerIcon,
ShieldOffIcon,
SwordIcon,
WaypointsIcon,
ZapIcon,
} from "lucide-react";
import { type JSX, useCallback, useRef, useState } from "react";
import { ArrowUpRightIcon, InfoIcon, ServerIcon } from "lucide-react";
import { useCallback, useEffect, useMemo } from "react";
import { Controller, type UseFormReturn, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { AntSwordTabContent } from "@/components/memshell/tabs/antsword-tab";
@@ -51,17 +40,7 @@ import type { MemShellFormSchema } from "@/types/schema";
import { Spinner } from "../ui/spinner";
import { JREVersionFormField } from "./jreversion-field";
import { ServerVersionFormField } from "./serverversion-field";
const shellToolIcons: Record<ShellToolType, JSX.Element> = {
[ShellToolType.Behinder]: <ShieldOffIcon className="h-4 w-4" />,
[ShellToolType.Godzilla]: <AxeIcon className="h-4 w-4" />,
[ShellToolType.Command]: <CommandIcon className="h-4 w-4" />,
[ShellToolType.AntSword]: <SwordIcon className="h-4 w-4" />,
[ShellToolType.Suo5]: <WaypointsIcon className="h-4 w-4" />,
[ShellToolType.Suo5v2]: <WaypointsIcon className="h-4 w-4" />,
[ShellToolType.NeoreGeorg]: <NetworkIcon className="h-4 w-4" />,
[ShellToolType.Custom]: <ZapIcon className="h-4 w-4" />,
};
import { ProxyTabContent } from "./tabs/proxy-tab";
export default function MainConfigCard({
mainConfig,
@@ -74,62 +53,108 @@ export default function MainConfigCard({
}>) {
const { t } = useTranslation(["common", "memshell"]);
const [shellToolMap, setShellToolMap] = useState<{
[toolName: string]: string[];
}>();
const [shellTools, setShellTools] = useState<ShellToolType[]>([]);
const [shellTypes, setShellTypes] = useState<string[]>([]);
const server = useWatch({
control: form.control,
name: "server",
});
const shellTool = useWatch({
control: form.control,
name: "shellTool",
});
const handleServerChange = useCallback(
(value: string) => {
if (mainConfig) {
const newShellToolMap = mainConfig[value];
setShellToolMap(newShellToolMap);
const serverToolMap = useMemo(() => {
if (!mainConfig || !server) {
return undefined;
}
return mainConfig[server];
}, [mainConfig, server]);
const newShellTools = Object.keys(newShellToolMap);
setShellTools([
...newShellTools.map((tool) => tool as ShellToolType),
ShellToolType.Custom,
]);
const serverOptions = useMemo(() => Object.keys(servers ?? {}), [servers]);
const currentShellTool = form.getValues("shellTool");
const shellTools = useMemo(() => {
if (!serverToolMap) {
return [];
}
const tools = Object.keys(serverToolMap).map(
(tool) => tool as ShellToolType,
);
return Array.from(new Set([...tools, ShellToolType.Custom]));
}, [serverToolMap]);
const firstTool = newShellTools[0];
let currentShellTypes = null;
const customShellTypes = useMemo(() => {
if (!server) {
return [];
}
return servers?.[server] ?? [];
}, [server, servers]);
if (!newShellToolMap[currentShellTool]) {
form.setValue("shellTool", firstTool);
currentShellTypes = newShellToolMap[firstTool];
} else {
currentShellTypes = newShellToolMap[currentShellTool];
const shellTypes = useMemo(() => {
if (!serverToolMap || !server) {
return [];
}
if (shellTool === ShellToolType.Custom) {
return customShellTypes;
}
return serverToolMap[shellTool] ?? [];
}, [customShellTypes, server, serverToolMap, shellTool]);
useEffect(() => {
if (!mainConfig || !server) {
return;
}
const toolMap = mainConfig[server];
if (!toolMap) {
return;
}
const toolKeys = Object.keys(toolMap);
if (toolKeys.length === 0) {
return;
}
const currentShellTool = form.getValues("shellTool") as ShellToolType;
const nextShellTool = toolMap[currentShellTool]
? currentShellTool
: (toolKeys[0] as ShellToolType);
if (nextShellTool !== currentShellTool) {
form.setValue("shellTool", nextShellTool);
}
if (nextShellTool !== ShellToolType.Custom) {
const nextShellTypes = toolMap[nextShellTool] ?? [];
if (nextShellTypes.length > 0) {
const currentShellType = form.getValues("shellType");
if (currentShellType !== nextShellTypes[0]) {
form.setValue("shellType", nextShellTypes[0]);
}
setShellTypes(currentShellTypes);
if (currentShellTypes && currentShellTypes.length > 0) {
form.setValue("shellType", currentShellTypes[0]);
}
if (
(value === "SpringWebFlux" || value === "XXLJOB") &&
Number.parseInt(form.getValues("targetJdkVersion") as string, 10) < 52
) {
form.setValue("targetJdkVersion", "52");
} else {
form.setValue("targetJdkVersion", "50");
}
form.resetField("serverVersion");
form.resetField("byPassJavaModule");
form.resetField("urlPattern");
}
},
[form, mainConfig],
);
}
const currentTargetJdk = form.getValues("targetJdkVersion") as string;
const currentJdkVersion = Number.parseInt(currentTargetJdk, 10);
const shouldRaiseJdkVersion =
(server === "SpringWebFlux" || server === "XXLJOB") &&
currentJdkVersion < 52;
const nextJdkVersion = shouldRaiseJdkVersion ? "52" : "50";
if (currentTargetJdk !== nextJdkVersion) {
form.setValue("targetJdkVersion", nextJdkVersion);
}
form.resetField("serverVersion");
form.resetField("byPassJavaModule");
form.resetField("urlPattern");
}, [form, mainConfig, server]);
useEffect(() => {
if (shellTool !== ShellToolType.Custom || customShellTypes.length === 0) {
return;
}
const currentShellType = form.getValues("shellType");
if (currentShellType !== customShellTypes[0]) {
form.setValue("shellType", customShellTypes[0]);
}
}, [customShellTypes, form, shellTool]);
const handleShellToolChange = useCallback(
(value: string) => {
@@ -172,16 +197,15 @@ export default function MainConfigCard({
form.resetField("shellClassBase64");
};
if (shellToolMap) {
if (serverToolMap) {
let currentShellTypes = null;
if (value === ShellToolType.Custom) {
currentShellTypes = servers?.[form.getValues("server")] as string[];
currentShellTypes = customShellTypes;
} else {
currentShellTypes = shellToolMap[value];
currentShellTypes = serverToolMap[value];
}
setShellTypes(currentShellTypes);
// 直接设置 shellType 而不是依赖 useEffect
// Set shellType directly instead of relying on useEffect.
if (currentShellTypes && currentShellTypes.length > 0) {
form.setValue("shellType", currentShellTypes[0]);
}
@@ -207,18 +231,9 @@ export default function MainConfigCard({
}
form.setValue("shellTool", value);
},
[form, servers, shellToolMap],
[customShellTypes, form, serverToolMap],
);
const initializedRef = useRef(false);
if (!initializedRef.current && mainConfig) {
const initialServer = form.getValues("server");
if (initialServer && mainConfig[initialServer]) {
handleServerChange(initialServer);
initializedRef.current = true;
}
}
return (
<>
<Card>
@@ -249,10 +264,7 @@ export default function MainConfigCard({
{t("common:server")}
</FieldLabel>
<Select
onValueChange={(v) => {
field.onChange(v);
handleServerChange(v as string);
}}
onValueChange={field.onChange}
value={field.value}
>
<SelectTrigger id="server">
@@ -261,13 +273,14 @@ export default function MainConfigCard({
/>
</SelectTrigger>
<SelectContent>
{Object.keys(servers ?? {}).map(
(server: string) => (
<SelectItem key={server} value={server}>
{server}
</SelectItem>
),
)}
{serverOptions.map((serverOption) => (
<SelectItem
key={serverOption}
value={serverOption}
>
{serverOption}
</SelectItem>
))}
</SelectContent>
</Select>
<FieldDescription className="flex items-center">
@@ -312,10 +325,7 @@ export default function MainConfigCard({
<SelectContent>
{shellTools.map((tool) => (
<SelectItem key={tool} value={tool}>
<span className="flex items-center gap-2">
{shellToolIcons[tool]}
{tool}
</span>
{tool}
</SelectItem>
))}
</SelectContent>
@@ -484,6 +494,7 @@ export default function MainConfigCard({
/>
<NeoRegTabContent form={form} shellTypes={shellTypes} />
<CustomTabContent form={form} shellTypes={shellTypes} />
<ProxyTabContent form={form} shellTypes={shellTypes} />
</Tabs>
)}
</>
@@ -11,6 +11,7 @@ import {
type GodzillaShellToolConfig,
type MemShellResult,
type NeoreGeorgShellToolConfig,
type ProxyShellToolConfig,
ShellToolType,
type Suo5ShellToolConfig,
} from "@/types/memshell";
@@ -163,6 +164,13 @@ export function BasicInfo({
value={`${(generateResult?.shellToolConfig as Suo5ShellToolConfig).headerName}: ${(generateResult?.shellToolConfig as Suo5ShellToolConfig).headerValue}`}
/>
)}
{generateResult?.shellConfig.shellTool === ShellToolType.Proxy && (
<CopyableField
label={t("shellToolConfig.httpHeader")}
text={`${(generateResult?.shellToolConfig as ProxyShellToolConfig).headerName}: ${(generateResult?.shellToolConfig as ProxyShellToolConfig).headerValue}`}
value={`${(generateResult?.shellToolConfig as ProxyShellToolConfig).headerName}: ${(generateResult?.shellToolConfig as ProxyShellToolConfig).headerValue}`}
/>
)}
{generateResult?.shellConfig.shellTool === ShellToolType.AntSword && (
<>
<CopyableField
@@ -17,7 +17,7 @@ export function FeedbackAlert() {
const { t } = useTranslation("memshell");
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<AlertDialogTrigger>
<Button variant="outline" type="button">
<CircleHelpIcon /> {t("shellNotWork.title")}
</Button>
@@ -1,5 +1,5 @@
import { DownloadIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import CodeViewer from "@/components/code-viewer";
import { Button } from "@/components/ui/button";
@@ -25,30 +25,36 @@ export function MultiPackResult({
}>) {
const showCode = packMethod === "JSP";
const { t } = useTranslation();
const packMethods = Object.keys(allPackResults ?? {});
const [selectedMethod, setSelectedMethod] = useState(packMethods[0]);
const [packResult, setPackResult] = useState(
allPackResults?.[selectedMethod as keyof typeof allPackResults] ?? "",
const packResults = allPackResults as Record<string, string> | undefined;
const packMethods = useMemo(
() => Object.keys(packResults ?? {}),
[packResults],
);
useEffect(() => {
const newPackMethods = Object.keys(allPackResults ?? {});
if (!newPackMethods.includes(selectedMethod)) {
const newSelectedMethod = newPackMethods[0];
setSelectedMethod(newSelectedMethod);
setPackResult(
allPackResults?.[newSelectedMethod as keyof typeof allPackResults] ??
"",
);
} else {
setPackResult(
allPackResults?.[selectedMethod as keyof typeof allPackResults] ?? "",
);
}
}, [allPackResults, selectedMethod]);
const [selectedMethod, setSelectedMethod] = useState(
() => packMethods[0] ?? "",
);
const handleDownload = () => {
const packResult = useMemo(() => {
if (!selectedMethod) {
return "";
}
return packResults?.[selectedMethod] ?? "";
}, [packResults, selectedMethod]);
useEffect(() => {
if (packMethods.length === 0) {
if (selectedMethod !== "") {
setSelectedMethod("");
}
return;
}
if (!packMethods.includes(selectedMethod)) {
setSelectedMethod(packMethods[0]);
}
}, [packMethods, selectedMethod]);
const handleDownload = useCallback(() => {
const fileName =
shellClassName?.substring(shellClassName?.lastIndexOf(".") ?? 0) ?? "";
if (packMethod === "JSP") {
@@ -64,13 +70,17 @@ export function MultiPackResult({
});
return downloadContent(content, fileName, ".data");
} else if (packMethod === "Base64") {
const base64Content =
allPackResults?.[
Object.keys(allPackResults)[0] as keyof typeof allPackResults
] ?? "";
const base64Content = packResults?.[packMethods[0]] ?? "";
return downloadBytes(base64Content, shellClassName);
}
};
}, [
packMethod,
packMethods,
packResult,
packResults,
selectedMethod,
shellClassName,
]);
return (
<CodeViewer
@@ -80,9 +90,6 @@ export function MultiPackResult({
<Select
onValueChange={(value) => {
setSelectedMethod(value as string);
setPackResult(
allPackResults?.[value as keyof typeof allPackResults] ?? "",
);
}}
value={selectedMethod}
>
@@ -0,0 +1,69 @@
import { Controller, type UseFormReturn, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card";
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { TabsContent } from "@/components/ui/tabs";
import type { MemShellFormSchema } from "@/types/schema";
import { OptionalClassFormField } from "./classname-field";
import { ShellTypeFormField } from "./shelltype-field";
export function ProxyTabContent({
form,
shellTypes,
}: Readonly<{
form: UseFormReturn<MemShellFormSchema>;
shellTypes: Array<string>;
}>) {
const shellType = useWatch({
name: "shellType",
control: form.control,
});
const { t } = useTranslation(["memshell", "common"]);
return (
<TabsContent value="Proxy">
<Card>
<CardContent className="space-y-2 mt-4">
<ShellTypeFormField form={form} shellTypes={shellTypes} />
<div
className="grid grid-cols-1 md:grid-cols-2 gap-2"
hidden={
shellType !== "BypassNginxWebSocket" &&
shellType !== "BypassNginxJakartaWebSocket"
}
>
<Controller
control={form.control}
name="headerName"
render={({ field }) => (
<Field className="gap-1">
<FieldLabel>{t("common:headerName")}</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
</Field>
)}
/>
<Controller
control={form.control}
name="headerValue"
render={({ field }) => (
<Field className="gap-1">
<FieldLabel>
{t("common:headerValue")} {t("common:optional")}
</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
</Field>
)}
/>
</div>
<OptionalClassFormField form={form} />
</CardContent>
</Card>
</TabsContent>
);
}
+2 -1
View File
@@ -12,6 +12,7 @@ export function BasicInfo({
console.log(generateResult);
const isBodyContent =
generateResult?.probeConfig.probeMethod === "ResponseBody";
const isFilterContent = generateResult?.probeConfig.probeContent === "Filter";
const isBodyCommand =
isBodyContent && generateResult?.probeConfig.probeContent === "Command";
return (
@@ -27,7 +28,7 @@ export function BasicInfo({
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 gap-2">
{isBodyContent && (
{!isFilterContent && isBodyContent && (
<CopyableField
label={t("common:paramName")}
value={
@@ -10,7 +10,6 @@ import {
FieldLabel,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
@@ -19,7 +18,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { SwitchField } from "@/components/ui/switch-field";
import {
Tooltip,
TooltipContent,
@@ -28,6 +27,11 @@ import {
import type { ServerConfig } from "@/types/memshell";
import type { ProbeShellFormSchema } from "@/types/schema";
// Hoisted static JSX to avoid recreation on each render (rendering-hoist-jsx)
const infoIcon = (
<InfoIcon className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
);
const PROBE_OPTIONS = [
{ value: "Server" as const, label: "server" },
{ value: "JDK" as const, label: "jdk" },
@@ -251,117 +255,35 @@ export default function MainConfigCard({ form, servers }: MainConfigCardProps) {
/>
)}
<div className="flex gap-4 mt-4 flex-col lg:grid lg:grid-cols-2 2xl:grid 2xl:grid-cols-3">
<Controller
<SwitchField
control={form.control}
name="debug"
render={({ field }) => (
<div className="flex items-center gap-2">
<Switch
id="debug"
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label htmlFor="debug">{t("common:debug")}</Label>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>{t("common:debug.description")}</p>
</TooltipContent>
</Tooltip>
</div>
)}
label={t("common:debug")}
description={t("common:debug.description")}
/>
<Controller
<SwitchField
control={form.control}
name="byPassJavaModule"
render={({ field }) => (
<div className="flex items-center gap-2">
<Switch
id="bypass"
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label htmlFor="bypass">{t("common:byPassJavaModule")}</Label>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>{t("common:byPassJavaModule.description")}</p>
</TooltipContent>
</Tooltip>
</div>
)}
label={t("common:byPassJavaModule")}
description={t("common:byPassJavaModule.description")}
/>
<Controller
<SwitchField
control={form.control}
name="lambdaSuffix"
render={({ field }) => (
<div className="flex items-center gap-2">
<Switch
id="lambdaSuffix"
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label htmlFor="lambdaSuffix">{t("common:lambdaSuffix")}</Label>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>{t("common:lambdaSuffix.description")}</p>
</TooltipContent>
</Tooltip>
</div>
)}
label={t("common:lambdaSuffix")}
description={t("common:lambdaSuffix.description")}
/>
<Controller
<SwitchField
control={form.control}
name="shrink"
render={({ field }) => (
<div className="flex items-center gap-2">
<Switch
id="shrink"
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label htmlFor="shrink">{t("common:shrink")}</Label>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>{t("common:shrink.description")}</p>
</TooltipContent>
</Tooltip>
</div>
)}
label={t("common:shrink")}
description={t("common:shrink.description")}
/>
<Controller
<SwitchField
control={form.control}
name="staticInitialize"
render={({ field }) => (
<div className="flex items-center gap-2">
<Switch
id="staticInitialize"
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label htmlFor="staticInitialize">
{t("common:staticInitialize")}
</Label>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>{t("common:staticInitialize.description")}</p>
</TooltipContent>
</Tooltip>
</div>
)}
label={t("common:staticInitialize")}
description={t("common:staticInitialize.description")}
/>
</div>
{isBodyMethod && needParam && (
@@ -376,9 +298,7 @@ export default function MainConfigCard({ form, servers }: MainConfigCardProps) {
{t("common:paramName")} {t("common:optional")}
</FieldLabel>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipTrigger>{infoIcon}</TooltipTrigger>
<TooltipContent>
<p>{t("common:paramName.description")}</p>
</TooltipContent>
+59
View File
@@ -0,0 +1,59 @@
import { InfoIcon } from "lucide-react";
import { memo } from "react";
import {
type Control,
Controller,
type FieldValues,
type Path,
} from "react-hook-form";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
// Hoisted static JSX to avoid recreation on each render
const infoIcon = (
<InfoIcon className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
);
interface SwitchFieldProps<T extends FieldValues> {
readonly name: Path<T>;
readonly label: string;
readonly description: string;
readonly control: Control<T>;
}
function SwitchFieldInner<T extends FieldValues>({
name,
label,
description,
control,
}: SwitchFieldProps<T>) {
return (
<Controller
control={control}
name={name}
render={({ field }) => (
<div className="flex items-center gap-2">
<Switch
id={name}
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label htmlFor={name}>{label}</Label>
<Tooltip>
<TooltipTrigger>{infoIcon}</TooltipTrigger>
<TooltipContent>
<p>{description}</p>
</TooltipContent>
</Tooltip>
</div>
)}
/>
);
}
export const SwitchField = memo(SwitchFieldInner) as typeof SwitchFieldInner;
+65 -44
View File
@@ -1,7 +1,7 @@
import { useQuery } from "@tanstack/react-query";
import { HomeLayout } from "fumadocs-ui/layouts/home";
import { LoaderCircle, WandSparklesIcon } from "lucide-react";
import { useState, useTransition } from "react";
import { useCallback, useState, useTransition } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
@@ -28,57 +28,70 @@ import {
import { transformToPostData } from "@/utils/transformer";
import { baseOptions } from "../lib/layout.shared";
const homeLayoutOptions = baseOptions();
const defaultValues: MemShellFormSchema = {
server: "Tomcat",
serverVersion: "Unknown",
targetJdkVersion: "50",
debug: false,
byPassJavaModule: false,
shellClassName: "",
shellTool: ShellToolType.Godzilla,
shellType: "Listener",
urlPattern: "/*",
godzillaPass: "",
godzillaKey: "",
commandParamName: "",
behinderPass: "",
antSwordPass: "",
headerName: "User-Agent",
headerValue: "",
injectorClassName: "",
packingMethod: "",
shrink: true,
staticInitialize: true,
shellClassBase64: "",
};
const jsonHeaders = {
"Content-Type": "application/json",
} as const;
const fetchJson = async <T,>(url: string): Promise<T> => {
const response = await fetch(url);
return response.json() as Promise<T>;
};
const fetchServerConfig = () =>
fetchJson<ServerConfig>(`${env.API_URL}/api/config/servers`);
const fetchMainConfig = () =>
fetchJson<MainConfig>(`${env.API_URL}/api/config`);
const fetchPackerConfig = () =>
fetchJson<PackerConfig>(`${env.API_URL}/api/config/packers`);
export default function MemShellPage() {
const { data: serverConfig } = useQuery<ServerConfig>({
queryKey: ["serverConfig"],
queryFn: async () => {
const response = await fetch(`${env.API_URL}/api/config/servers`);
return await response.json();
},
queryFn: fetchServerConfig,
});
const { data: mainConfig } = useQuery<MainConfig>({
queryKey: ["mainConfig"],
queryFn: async () => {
const response = await fetch(`${env.API_URL}/api/config`);
return await response.json();
},
queryFn: fetchMainConfig,
});
const { data: packerConfig } = useQuery<PackerConfig>({
queryKey: ["packerConfig"],
queryFn: async () => {
const response = await fetch(`${env.API_URL}/api/config/packers`);
return await response.json();
},
queryFn: fetchPackerConfig,
});
const { t } = useTranslation(["common", "memshell"]);
const form = useForm({
resolver: useYupValidationResolver(memShellFormSchema, t),
defaultValues: {
server: "Tomcat",
serverVersion: "Unknown",
targetJdkVersion: "50",
debug: false,
byPassJavaModule: false,
shellClassName: "",
shellTool: ShellToolType.Godzilla,
shellType: "Listener",
urlPattern: "/*",
godzillaPass: "",
godzillaKey: "",
commandParamName: "",
behinderPass: "",
antSwordPass: "",
headerName: "User-Agent",
headerValue: "",
injectorClassName: "",
packingMethod: "",
shrink: true,
staticInitialize: true,
shellClassBase64: "",
},
defaultValues,
});
const [packResult, setPackResult] = useState<string | undefined>();
@@ -89,15 +102,13 @@ export default function MemShellPage() {
const [packMethod, setPackMethod] = useState<string>("");
const [isActionPending, startTransition] = useTransition();
const onSubmit = async (data: MemShellFormSchema) => {
startTransition(async () => {
const submitMemShell = useCallback(
async (data: MemShellFormSchema) => {
try {
const postData = transformToPostData(data);
const response = await fetch(`${env.API_URL}/api/memshell/generate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
headers: jsonHeaders,
body: JSON.stringify(postData),
});
@@ -118,11 +129,21 @@ export default function MemShellPage() {
t("toast.generateError", { error: (error as Error).message }),
);
}
});
};
},
[t],
);
const onSubmit = useCallback(
(data: MemShellFormSchema) => {
startTransition(() => {
void submitMemShell(data);
});
},
[submitMemShell],
);
return (
<HomeLayout {...baseOptions()} links={siteConfig.navLinks}>
<HomeLayout {...homeLayoutOptions} links={siteConfig.navLinks}>
<div className="container mx-auto max-w-8xl p-6">
<form
onSubmit={form.handleSubmit(onSubmit)}
+7
View File
@@ -55,6 +55,12 @@ export interface Suo5ShellToolConfig {
headerValue?: string;
}
export interface ProxyShellToolConfig {
shellClassName?: string;
headerName?: string;
headerValue?: string;
}
export interface AntSwordShellToolConfig {
shellClassName?: string;
pass?: string;
@@ -137,4 +143,5 @@ export enum ShellToolType {
Suo5v2 = "Suo5v2",
NeoreGeorg = "NeoreGeorg",
Custom = "Custom",
Proxy = "Proxy",
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

+9 -1
View File
@@ -1,4 +1,12 @@
{
"title": "内存马工具",
"pages": ["godzilla", "suo5", "behinder", "command", "antsword", "neoregeorg"]
"pages": [
"godzilla",
"suo5",
"behinder",
"command",
"antsword",
"neoregeorg",
"proxy"
]
}
+101
View File
@@ -0,0 +1,101 @@
---
title: Proxy
---
import { Step, Steps } from 'fumadocs-ui/components/steps';
参考地址:https://github.com/veo/wsMemShell/blob/main/static/websocketproxy.md
## WebSocket 内存马
<Steps>
<Step>
### 选择 Proxy
![proxy_ws.png](../images/proxy_ws.png)
</Step>
<Step>
### 生成并注入
选取合适的打包方式,并进行内存马的注入。
</Step>
<Step>
### 使用 Gost 客户端尝试启动代理
https://github.com/go-gost/gost
```bash
./gost -L :1080 -F "ws://127.0.0.1:8082?path=/app/proxy"
{"handler":"auto","kind":"service","level":"info","listener":"tcp","msg":"listening on [::]:1080/tcp","service":"service-0","time":"2026-01-16T23:15:55.143+08:00"}
```
尝试使用 curl 命令使用代理访问百度
```bash
> curl -x socks5h://127.0.0.1:1080 https://www.baidu.com
```
</Step>
</Steps>
## BypassNginxWebSocket 内存马
<Steps>
<Step>
### 选择 Proxy 并填写参数
![proxy_bypass_ws.png](../images/proxy_bypass_ws.png)
</Step>
<Step>
### 生成并注入
选取合适的打包方式,并进行内存马的注入。
</Step>
<Step>
### 使用 Gost 客户端尝试启动代理
https://github.com/go-gost/gost
由于 Gost 自定义请求头只能通过配置文件实现,因此创建一个 `gost.yaml`
```yaml
services:
- name: service-0
addr: :1080
handler:
type: auto
listener:
type: tcp
chain: chain-0
chains:
- name: chain-0
hops:
- name: hop-ws
nodes:
- name: ws-tunnel
addr: 127.0.0.1:80 # 此处填写目标地址,我是用 Nginx 反代所以是 80 端口
connector:
type: http
dialer:
type: ws
metadata:
path: /app/bypass-proxy # 此处填写 WebSocket 路径
header:
User-Agent: "test" # 此处填写自定义请求头
```
```bash
./gost -C gost.yaml
{"handler":"auto","kind":"service","level":"info","listener":"tcp","msg":"listening on [::]:1080/tcp","service":"service-0","time":"2026-01-16T23:30:04.927+08:00"}
```
尝试使用 curl 命令使用代理访问百度
```bash
> curl -x socks5h://127.0.0.1:1080 https://www.baidu.com
```
</Step>
</Steps>