Skip to content

Commit 7b738a5

Browse files
committed
refactor: use yup custom validator
1 parent 0e5f8cc commit 7b738a5

File tree

8 files changed

+177
-73
lines changed

8 files changed

+177
-73
lines changed

web/bun.lockb

584 Bytes
Binary file not shown.

web/package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"@types/react-copy-to-clipboard": "^5.0.7",
2121
"@types/react-dom": "^19.1.5",
2222
"@types/react-syntax-highlighter": "^15.5.13",
23-
"@vitejs/plugin-react": "^4.4.1",
23+
"@vitejs/plugin-react": "^4.5.0",
2424
"rimraf": "^6.0.1",
2525
"tailwindcss": "^4.1.7",
2626
"typescript": "^5.8.3",
@@ -56,32 +56,32 @@
5656
"@radix-ui/react-toggle-group": "^1.1.10",
5757
"@radix-ui/react-tooltip": "^1.2.7",
5858
"@tailwindcss/vite": "^4.1.7",
59-
"@tanstack/react-query": "^5.76.1",
59+
"@tanstack/react-query": "^5.77.2",
6060
"class-variance-authority": "^0.7.1",
6161
"clsx": "^2.1.1",
6262
"cmdk": "^1.1.1",
6363
"date-fns": "^4.1.0",
6464
"embla-carousel-react": "^8.6.0",
65-
"i18next": "^25.2.0",
65+
"i18next": "^25.2.1",
6666
"input-otp": "^1.4.2",
6767
"lucide-react": "^0.511.0",
6868
"react": "^19.1.0",
6969
"react-copy-to-clipboard": "^5.1.0",
7070
"react-day-picker": "9.7.0",
7171
"react-dom": "^19.1.0",
7272
"react-hook-form": "^7.56.4",
73-
"react-i18next": "^15.5.1",
73+
"react-i18next": "^15.5.2",
7474
"react-resizable-panels": "^3.0.2",
75-
"react-router": "^7.6.0",
76-
"react-router-dom": "^7.6.0",
75+
"react-router": "^7.6.1",
76+
"react-router-dom": "^7.6.1",
7777
"react-syntax-highlighter": "^15.6.1",
7878
"recharts": "^2.15.3",
7979
"sonner": "^2.0.3",
8080
"tailwind-merge": "^3.3.0",
8181
"tailwind-scrollbar": "^4.0.2",
8282
"tailwindcss-animate": "^1.0.7",
8383
"vaul": "^1.1.2",
84-
"zod": "^3.25.20"
84+
"yup": "^1.6.1"
8585
},
8686
"trustedDependencies": ["@biomejs/biome"]
8787
}

web/src/config.ts

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,51 @@
1-
import * as z from "zod";
1+
import * as yup from "yup";
22

3-
const EnvSchema = z.object({
4-
API_URL: z.optional(z.string()),
5-
BASE_PATH: z.optional(z.string()),
6-
MODE: z.string(),
3+
const EnvSchema = yup.object({
4+
API_URL: yup.string().optional(),
5+
BASE_PATH: yup.string().optional(),
6+
MODE: yup.string().required(),
77
});
88

9+
type EnvSchema = yup.InferType<typeof EnvSchema>;
10+
function safeParseYup<T>(schema: yup.ObjectSchema<any>, data: unknown) {
11+
try {
12+
const validatedData = schema.validateSync(data, {
13+
abortEarly: false,
14+
stripUnknown: true
15+
});
16+
17+
return {
18+
success: true as const,
19+
data: validatedData as T,
20+
error: undefined,
21+
};
22+
} catch (error) {
23+
if (error instanceof yup.ValidationError) {
24+
return {
25+
success: false as const,
26+
data: undefined,
27+
error: {
28+
issues: error.inner.map(err => ({
29+
path: err.path?.split('.') || [],
30+
message: err.message,
31+
code: err.type ?? 'validation_error',
32+
})),
33+
message: error.message,
34+
},
35+
};
36+
}
37+
38+
return {
39+
success: false as const,
40+
data: undefined,
41+
error: {
42+
issues: [],
43+
message: 'Unknown validation error',
44+
},
45+
};
46+
}
47+
}
48+
949
const createEnv = () => {
1050
// @ts-ignore
1151
const envVars = Object.entries(import.meta.env).reduce<Record<string, string>>((acc, curr) => {
@@ -20,14 +60,15 @@ const createEnv = () => {
2060
}
2161
return acc;
2262
}, {});
23-
const parsedEnv = EnvSchema.safeParse(envVars);
63+
console.log(envVars)
64+
const parsedEnv = safeParseYup<EnvSchema>(EnvSchema, envVars);
2465
if (!parsedEnv.success) {
2566
throw new Error(
2667
`Invalid env provided.
2768
The following variables are missing or invalid:
28-
${Object.entries(parsedEnv.error.flatten().fieldErrors)
29-
.map(([k, v]) => `- ${k}: ${v}`)
30-
.join("\n")}
69+
${parsedEnv.error.issues
70+
.map(({ path, message }) => `- ${path}: ${message}`)
71+
.join("\n")}
3172
`,
3273
);
3374
}

web/src/i18n/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@
130130
"agent-move-to-target": "Move MemShellAgent.jar and jattach to target host",
131131
"agent-move-to-target1": "Move MemShellAgent.jar to target host",
132132
"servletUrlPattern": "Servlet type requires a specific URL Pattern, e.g., /hello_servlet",
133+
"specificUrlPattern": "URL Pattern must be specified, e.g., /hello",
133134
"shellBytesEmpty": "Shell bytes is empty, please generate shell first",
134135
"shellToolNotSelected": "Please select a shell tool type first",
135136
"targetServerNotFound": "Target server not found?",
@@ -143,5 +144,6 @@
143144
"updateAvailableTooltip": "Click to Open Github Release Page ( v{{currentVersion}} -> v{{latestVersion}})"
144145
},
145146
"generator": "Generator",
146-
"about": "About"
147+
"about": "About",
148+
"classNameOptions": "classNameOptions"
147149
}

web/src/i18n/zh-CN.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,12 +136,14 @@
136136
"targetServerRequest": "请求适配",
137137
"try-to-use-shell": "尝试利用内存马",
138138
"waitingForGeneration": "// 等待填写参数生成中...",
139-
"customShellClass": "请输入自定义内存马类,base64 或类文件"
139+
"customShellClass": "请输入自定义内存马类,base64 或类文件",
140+
"specificUrlPattern": "必须指定 URL Pattern,例如 /hello"
140141
},
141142
"version": {
142143
"updateAvailable": "有可用升级",
143144
"updateAvailableTooltip": "点击前往 GitHub Release ( v{{currentVersion}} -> v{{latestVersion}})"
144145
},
145146
"about": "关于",
146-
"generator": "生成器"
147+
"generator": "生成器",
148+
"classNameOptions": "类名配置项"
147149
}

web/src/pages/index.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ShellResult } from "@/components/shell-result.tsx";
44
import { Button } from "@/components/ui/button";
55
import { Form } from "@/components/ui/form.tsx";
66
import { env } from "@/config.ts";
7-
import { FormSchema, formSchema } from "@/types/schema.ts";
7+
import { FormSchema, formSchema, useYupValidationResolver } from "@/types/schema.ts";
88
import {
99
APIErrorResponse,
1010
GenerateResponse,
@@ -14,8 +14,7 @@ import {
1414
ServerConfig,
1515
ShellToolType,
1616
} from "@/types/shell.ts";
17-
import { customValidation, transformToPostData } from "@/utils/transformer.ts";
18-
import { zodResolver } from "@hookform/resolvers/zod";
17+
import { transformToPostData } from "@/utils/transformer.ts";
1918
import { useQuery } from "@tanstack/react-query";
2019
import { LoaderCircle, WandSparklesIcon } from "lucide-react";
2120
import { useState, useTransition } from "react";
@@ -52,8 +51,8 @@ export default function IndexPage() {
5251
});
5352

5453
const { t } = useTranslation();
55-
const form = useForm<FormSchema>({
56-
resolver: zodResolver(formSchema),
54+
const form = useForm({
55+
resolver: useYupValidationResolver(formSchema, t),
5756
defaultValues: {
5857
server: urlParams.server ?? "Tomcat",
5958
targetJdkVersion: urlParams.targetJdkVersion ?? "50",
@@ -86,7 +85,6 @@ export default function IndexPage() {
8685
const onSubmit = async (data: FormSchema) => {
8786
startTransition(async () => {
8887
try {
89-
customValidation(t, data);
9088
const postData = transformToPostData(data);
9189
const response = await fetch(`${env.API_URL}/generate`, {
9290
method: "POST",

web/src/types/schema.ts

Lines changed: 108 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,110 @@
1-
import * as z from "zod";
2-
3-
export const formSchema = z.object({
4-
server: z.string().min(1),
5-
targetJdkVersion: z.optional(z.string()),
6-
debug: z.optional(z.boolean()),
7-
bypassJavaModule: z.optional(z.boolean()),
8-
shellClassName: z.string().optional(),
9-
shellTool: z.string().min(1),
10-
shellType: z.string().min(1),
11-
urlPattern: z.optional(z.string()),
12-
godzillaPass: z.optional(z.string()),
13-
godzillaKey: z.optional(z.string()),
14-
behinderPass: z.optional(z.string()),
15-
antSwordPass: z.optional(z.string()),
16-
commandParamName: z.optional(z.string()),
17-
implementationClass: z.optional(z.string()),
18-
headerName: z.optional(z.string()),
19-
headerValue: z.optional(z.string()),
20-
injectorClassName: z.optional(z.string()),
21-
packingMethod: z.string().min(1),
22-
shrink: z.optional(z.boolean()),
23-
shellClassBase64: z.optional(z.string()),
24-
encryptor: z.optional(z.string()),
1+
import { TFunction } from "i18next";
2+
import { useCallback } from "react";
3+
import { FieldErrors } from "react-hook-form";
4+
import * as yup from "yup";
5+
import { ShellToolType } from "./shell";
6+
7+
export const formSchema = yup.object({
8+
server: yup.string().required().min(1),
9+
targetJdkVersion: yup.string().optional(),
10+
debug: yup.boolean().optional(),
11+
bypassJavaModule: yup.boolean().optional(),
12+
shellClassName: yup.string().optional(),
13+
shellTool: yup.string().required().min(1),
14+
shellType: yup.string().required().min(1),
15+
urlPattern: yup.string().optional(),
16+
godzillaPass: yup.string().optional(),
17+
godzillaKey: yup.string().optional(),
18+
behinderPass: yup.string().optional(),
19+
antSwordPass: yup.string().optional(),
20+
commandParamName: yup.string().optional(),
21+
implementationClass: yup.string().optional(),
22+
headerName: yup.string().optional(),
23+
headerValue: yup.string().optional(),
24+
injectorClassName: yup.string().optional(),
25+
packingMethod: yup.string().required().min(1),
26+
shrink: yup.boolean().optional(),
27+
shellClassBase64: yup.string().optional(),
28+
encryptor: yup.string().optional(),
2529
});
2630

27-
export type FormSchema = z.infer<typeof formSchema>;
31+
interface ValidationResult {
32+
values: FormSchema;
33+
errors: FieldErrors<FormSchema>;
34+
}
35+
36+
const urlPatternIsNeeded = (shellType: string) => {
37+
return (
38+
shellType.endsWith("Servlet") ||
39+
shellType.endsWith("ControllerHandler") ||
40+
shellType === "HandlerMethod" ||
41+
shellType === "HandlerFunction" ||
42+
shellType.endsWith("WebSocket")
43+
);
44+
};
45+
46+
const isInvalidUrl = (urlPattern: string | undefined) =>
47+
urlPattern === "/" || urlPattern === "/*" || !urlPattern?.startsWith("/") || !urlPattern;
48+
49+
export const useYupValidationResolver = (validationSchema: yup.ObjectSchema<any>, t: TFunction) =>
50+
useCallback(
51+
async (data: FormSchema): Promise<ValidationResult> => {
52+
try {
53+
const values = (await validationSchema.validate(data, {
54+
abortEarly: false,
55+
})) as FormSchema;
56+
57+
const urlPattern: keyof FormSchema = "urlPattern";
58+
const shellClassBase64: keyof FormSchema = "shellClassBase64";
59+
const errors = {} as any;
60+
61+
if (urlPatternIsNeeded(values?.shellType) && isInvalidUrl(values?.urlPattern)) {
62+
errors[urlPattern] = {
63+
type: "custom",
64+
message: t("tips.specificUrlPattern"),
65+
};
66+
}
67+
if (values.shellTool === ShellToolType.Custom && !values.shellClassBase64) {
68+
errors[shellClassBase64] = {
69+
type: "custom",
70+
message: t("tips.customShellClass"),
71+
};
72+
}
73+
74+
return {
75+
values,
76+
errors,
77+
};
78+
} catch (errors) {
79+
if (errors instanceof yup.ValidationError) {
80+
return {
81+
values: {} as FormSchema,
82+
errors: errors.inner.reduce(
83+
(allErrors, currentError) => ({
84+
// biome-ignore lint/performance/noAccumulatingSpread: <explanation>
85+
...allErrors,
86+
[currentError.path as keyof FormSchema]: {
87+
type: currentError.type ?? "validation",
88+
message: currentError.message,
89+
},
90+
}),
91+
{},
92+
),
93+
};
94+
}
95+
96+
return {
97+
values: {} as FormSchema,
98+
errors: {
99+
server: {
100+
type: "unknown",
101+
message: "An unexpected validation error occurred",
102+
},
103+
},
104+
};
105+
}
106+
},
107+
[validationSchema, t],
108+
);
109+
110+
export type FormSchema = yup.InferType<typeof formSchema>;

web/src/utils/transformer.ts

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,5 @@
11
import { FormSchema } from "@/types/schema.ts";
2-
import { InjectorConfig, ShellConfig, ShellToolConfig, ShellToolType } from "@/types/shell.ts";
3-
import { TFunction } from "i18next";
4-
5-
export function customValidation(t: TFunction<"translation", undefined>, values: FormSchema) {
6-
if (values.shellType.endsWith("Servlet") && (values.urlPattern === "/*" || !values.urlPattern)) {
7-
throw new Error(t("tips.servletUrlPattern"));
8-
}
9-
10-
if (values.shellType.endsWith("ControllerHandler") && (values.urlPattern === "/*" || !values.urlPattern)) {
11-
throw new Error(t("tips.controllerUrlPattern"));
12-
}
13-
14-
if (
15-
(values.shellType === "HandlerMethod" || values.shellType === "HandlerFunction") &&
16-
(values.urlPattern === "/*" || !values.urlPattern)
17-
) {
18-
throw new Error(t("tips.handlerUrlPattern"));
19-
}
20-
21-
if (values.shellTool === ShellToolType.Custom && !values.shellClassBase64) {
22-
throw new Error(t("tips.customShellClass"));
23-
}
24-
}
2+
import { InjectorConfig, ShellConfig, ShellToolConfig } from "@/types/shell.ts";
253

264
export function transformToPostData(formValue: FormSchema) {
275
const shellConfig: ShellConfig = {

0 commit comments

Comments
 (0)