refactor: use yup custom validator

This commit is contained in:
ReaJason
2025-05-28 01:11:52 +08:00
parent cb81331f0e
commit 6e863d9dc9
8 changed files with 176 additions and 72 deletions
BIN
View File
Binary file not shown.
+7 -7
View File
@@ -20,7 +20,7 @@
"@types/react-copy-to-clipboard": "^5.0.7", "@types/react-copy-to-clipboard": "^5.0.7",
"@types/react-dom": "^19.1.5", "@types/react-dom": "^19.1.5",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"@vitejs/plugin-react": "^4.4.1", "@vitejs/plugin-react": "^4.5.0",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"tailwindcss": "^4.1.7", "tailwindcss": "^4.1.7",
"typescript": "^5.8.3", "typescript": "^5.8.3",
@@ -56,13 +56,13 @@
"@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7", "@radix-ui/react-tooltip": "^1.2.7",
"@tailwindcss/vite": "^4.1.7", "@tailwindcss/vite": "^4.1.7",
"@tanstack/react-query": "^5.76.1", "@tanstack/react-query": "^5.77.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"i18next": "^25.2.0", "i18next": "^25.2.1",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.511.0", "lucide-react": "^0.511.0",
"react": "^19.1.0", "react": "^19.1.0",
@@ -70,10 +70,10 @@
"react-day-picker": "9.7.0", "react-day-picker": "9.7.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-hook-form": "^7.56.4", "react-hook-form": "^7.56.4",
"react-i18next": "^15.5.1", "react-i18next": "^15.5.2",
"react-resizable-panels": "^3.0.2", "react-resizable-panels": "^3.0.2",
"react-router": "^7.6.0", "react-router": "^7.6.1",
"react-router-dom": "^7.6.0", "react-router-dom": "^7.6.1",
"react-syntax-highlighter": "^15.6.1", "react-syntax-highlighter": "^15.6.1",
"recharts": "^2.15.3", "recharts": "^2.15.3",
"sonner": "^2.0.3", "sonner": "^2.0.3",
@@ -81,7 +81,7 @@
"tailwind-scrollbar": "^4.0.2", "tailwind-scrollbar": "^4.0.2",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"zod": "^3.25.20" "yup": "^1.6.1"
}, },
"trustedDependencies": ["@biomejs/biome"] "trustedDependencies": ["@biomejs/biome"]
} }
+50 -9
View File
@@ -1,11 +1,51 @@
import * as z from "zod"; import * as yup from "yup";
const EnvSchema = z.object({ const EnvSchema = yup.object({
API_URL: z.optional(z.string()), API_URL: yup.string().optional(),
BASE_PATH: z.optional(z.string()), BASE_PATH: yup.string().optional(),
MODE: z.string(), MODE: yup.string().required(),
}); });
type EnvSchema = yup.InferType<typeof EnvSchema>;
function safeParseYup<T>(schema: yup.ObjectSchema<any>, data: unknown) {
try {
const validatedData = schema.validateSync(data, {
abortEarly: false,
stripUnknown: true
});
return {
success: true as const,
data: validatedData as T,
error: undefined,
};
} catch (error) {
if (error instanceof yup.ValidationError) {
return {
success: false as const,
data: undefined,
error: {
issues: error.inner.map(err => ({
path: err.path?.split('.') || [],
message: err.message,
code: err.type ?? 'validation_error',
})),
message: error.message,
},
};
}
return {
success: false as const,
data: undefined,
error: {
issues: [],
message: 'Unknown validation error',
},
};
}
}
const createEnv = () => { const createEnv = () => {
// @ts-ignore // @ts-ignore
const envVars = Object.entries(import.meta.env).reduce<Record<string, string>>((acc, curr) => { const envVars = Object.entries(import.meta.env).reduce<Record<string, string>>((acc, curr) => {
@@ -20,14 +60,15 @@ const createEnv = () => {
} }
return acc; return acc;
}, {}); }, {});
const parsedEnv = EnvSchema.safeParse(envVars); console.log(envVars)
const parsedEnv = safeParseYup<EnvSchema>(EnvSchema, envVars);
if (!parsedEnv.success) { if (!parsedEnv.success) {
throw new Error( throw new Error(
`Invalid env provided. `Invalid env provided.
The following variables are missing or invalid: The following variables are missing or invalid:
${Object.entries(parsedEnv.error.flatten().fieldErrors) ${parsedEnv.error.issues
.map(([k, v]) => `- ${k}: ${v}`) .map(({ path, message }) => `- ${path}: ${message}`)
.join("\n")} .join("\n")}
`, `,
); );
} }
+3 -1
View File
@@ -130,6 +130,7 @@
"agent-move-to-target": "Move MemShellAgent.jar and jattach to target host", "agent-move-to-target": "Move MemShellAgent.jar and jattach to target host",
"agent-move-to-target1": "Move MemShellAgent.jar to target host", "agent-move-to-target1": "Move MemShellAgent.jar to target host",
"servletUrlPattern": "Servlet type requires a specific URL Pattern, e.g., /hello_servlet", "servletUrlPattern": "Servlet type requires a specific URL Pattern, e.g., /hello_servlet",
"specificUrlPattern": "URL Pattern must be specified, e.g., /hello",
"shellBytesEmpty": "Shell bytes is empty, please generate shell first", "shellBytesEmpty": "Shell bytes is empty, please generate shell first",
"shellToolNotSelected": "Please select a shell tool type first", "shellToolNotSelected": "Please select a shell tool type first",
"targetServerNotFound": "Target server not found?", "targetServerNotFound": "Target server not found?",
@@ -143,5 +144,6 @@
"updateAvailableTooltip": "Click to Open Github Release Page ( v{{currentVersion}} -> v{{latestVersion}})" "updateAvailableTooltip": "Click to Open Github Release Page ( v{{currentVersion}} -> v{{latestVersion}})"
}, },
"generator": "Generator", "generator": "Generator",
"about": "About" "about": "About",
"classNameOptions": "classNameOptions"
} }
+4 -2
View File
@@ -136,12 +136,14 @@
"targetServerRequest": "请求适配", "targetServerRequest": "请求适配",
"try-to-use-shell": "尝试利用内存马", "try-to-use-shell": "尝试利用内存马",
"waitingForGeneration": "// 等待填写参数生成中...", "waitingForGeneration": "// 等待填写参数生成中...",
"customShellClass": "请输入自定义内存马类,base64 或类文件" "customShellClass": "请输入自定义内存马类,base64 或类文件",
"specificUrlPattern": "必须指定 URL Pattern,例如 /hello"
}, },
"version": { "version": {
"updateAvailable": "有可用升级", "updateAvailable": "有可用升级",
"updateAvailableTooltip": "点击前往 GitHub Release ( v{{currentVersion}} -> v{{latestVersion}})" "updateAvailableTooltip": "点击前往 GitHub Release ( v{{currentVersion}} -> v{{latestVersion}})"
}, },
"about": "关于", "about": "关于",
"generator": "生成器" "generator": "生成器",
"classNameOptions": "类名配置项"
} }
+4 -6
View File
@@ -4,7 +4,7 @@ import { ShellResult } from "@/components/shell-result.tsx";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Form } from "@/components/ui/form.tsx"; import { Form } from "@/components/ui/form.tsx";
import { env } from "@/config.ts"; import { env } from "@/config.ts";
import { FormSchema, formSchema } from "@/types/schema.ts"; import { FormSchema, formSchema, useYupValidationResolver } from "@/types/schema.ts";
import { import {
APIErrorResponse, APIErrorResponse,
GenerateResponse, GenerateResponse,
@@ -14,8 +14,7 @@ import {
ServerConfig, ServerConfig,
ShellToolType, ShellToolType,
} from "@/types/shell.ts"; } from "@/types/shell.ts";
import { customValidation, transformToPostData } from "@/utils/transformer.ts"; import { transformToPostData } from "@/utils/transformer.ts";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { LoaderCircle, WandSparklesIcon } from "lucide-react"; import { LoaderCircle, WandSparklesIcon } from "lucide-react";
import { useState, useTransition } from "react"; import { useState, useTransition } from "react";
@@ -52,8 +51,8 @@ export default function IndexPage() {
}); });
const { t } = useTranslation(); const { t } = useTranslation();
const form = useForm<FormSchema>({ const form = useForm({
resolver: zodResolver(formSchema), resolver: useYupValidationResolver(formSchema, t),
defaultValues: { defaultValues: {
server: urlParams.server ?? "Tomcat", server: urlParams.server ?? "Tomcat",
targetJdkVersion: urlParams.targetJdkVersion ?? "50", targetJdkVersion: urlParams.targetJdkVersion ?? "50",
@@ -86,7 +85,6 @@ export default function IndexPage() {
const onSubmit = async (data: FormSchema) => { const onSubmit = async (data: FormSchema) => {
startTransition(async () => { startTransition(async () => {
try { try {
customValidation(t, data);
const postData = transformToPostData(data); const postData = transformToPostData(data);
const response = await fetch(`${env.API_URL}/generate`, { const response = await fetch(`${env.API_URL}/generate`, {
method: "POST", method: "POST",
+107 -24
View File
@@ -1,27 +1,110 @@
import * as z from "zod"; import { TFunction } from "i18next";
import { useCallback } from "react";
import { FieldErrors } from "react-hook-form";
import * as yup from "yup";
import { ShellToolType } from "./shell";
export const formSchema = z.object({ export const formSchema = yup.object({
server: z.string().min(1), server: yup.string().required().min(1),
targetJdkVersion: z.optional(z.string()), targetJdkVersion: yup.string().optional(),
debug: z.optional(z.boolean()), debug: yup.boolean().optional(),
bypassJavaModule: z.optional(z.boolean()), bypassJavaModule: yup.boolean().optional(),
shellClassName: z.string().optional(), shellClassName: yup.string().optional(),
shellTool: z.string().min(1), shellTool: yup.string().required().min(1),
shellType: z.string().min(1), shellType: yup.string().required().min(1),
urlPattern: z.optional(z.string()), urlPattern: yup.string().optional(),
godzillaPass: z.optional(z.string()), godzillaPass: yup.string().optional(),
godzillaKey: z.optional(z.string()), godzillaKey: yup.string().optional(),
behinderPass: z.optional(z.string()), behinderPass: yup.string().optional(),
antSwordPass: z.optional(z.string()), antSwordPass: yup.string().optional(),
commandParamName: z.optional(z.string()), commandParamName: yup.string().optional(),
implementationClass: z.optional(z.string()), implementationClass: yup.string().optional(),
headerName: z.optional(z.string()), headerName: yup.string().optional(),
headerValue: z.optional(z.string()), headerValue: yup.string().optional(),
injectorClassName: z.optional(z.string()), injectorClassName: yup.string().optional(),
packingMethod: z.string().min(1), packingMethod: yup.string().required().min(1),
shrink: z.optional(z.boolean()), shrink: yup.boolean().optional(),
shellClassBase64: z.optional(z.string()), shellClassBase64: yup.string().optional(),
encryptor: z.optional(z.string()), encryptor: yup.string().optional(),
}); });
export type FormSchema = z.infer<typeof formSchema>; interface ValidationResult {
values: FormSchema;
errors: FieldErrors<FormSchema>;
}
const urlPatternIsNeeded = (shellType: string) => {
return (
shellType.endsWith("Servlet") ||
shellType.endsWith("ControllerHandler") ||
shellType === "HandlerMethod" ||
shellType === "HandlerFunction" ||
shellType.endsWith("WebSocket")
);
};
const isInvalidUrl = (urlPattern: string | undefined) =>
urlPattern === "/" || urlPattern === "/*" || !urlPattern?.startsWith("/") || !urlPattern;
export const useYupValidationResolver = (validationSchema: yup.ObjectSchema<any>, t: TFunction) =>
useCallback(
async (data: FormSchema): Promise<ValidationResult> => {
try {
const values = (await validationSchema.validate(data, {
abortEarly: false,
})) as FormSchema;
const urlPattern: keyof FormSchema = "urlPattern";
const shellClassBase64: keyof FormSchema = "shellClassBase64";
const errors = {} as any;
if (urlPatternIsNeeded(values?.shellType) && isInvalidUrl(values?.urlPattern)) {
errors[urlPattern] = {
type: "custom",
message: t("tips.specificUrlPattern"),
};
}
if (values.shellTool === ShellToolType.Custom && !values.shellClassBase64) {
errors[shellClassBase64] = {
type: "custom",
message: t("tips.customShellClass"),
};
}
return {
values,
errors,
};
} catch (errors) {
if (errors instanceof yup.ValidationError) {
return {
values: {} as FormSchema,
errors: errors.inner.reduce(
(allErrors, currentError) => ({
// biome-ignore lint/performance/noAccumulatingSpread: <explanation>
...allErrors,
[currentError.path as keyof FormSchema]: {
type: currentError.type ?? "validation",
message: currentError.message,
},
}),
{},
),
};
}
return {
values: {} as FormSchema,
errors: {
server: {
type: "unknown",
message: "An unexpected validation error occurred",
},
},
};
}
},
[validationSchema, t],
);
export type FormSchema = yup.InferType<typeof formSchema>;
+1 -23
View File
@@ -1,27 +1,5 @@
import { FormSchema } from "@/types/schema.ts"; import { FormSchema } from "@/types/schema.ts";
import { InjectorConfig, ShellConfig, ShellToolConfig, ShellToolType } from "@/types/shell.ts"; import { InjectorConfig, ShellConfig, ShellToolConfig } from "@/types/shell.ts";
import { TFunction } from "i18next";
export function customValidation(t: TFunction<"translation", undefined>, values: FormSchema) {
if (values.shellType.endsWith("Servlet") && (values.urlPattern === "/*" || !values.urlPattern)) {
throw new Error(t("tips.servletUrlPattern"));
}
if (values.shellType.endsWith("ControllerHandler") && (values.urlPattern === "/*" || !values.urlPattern)) {
throw new Error(t("tips.controllerUrlPattern"));
}
if (
(values.shellType === "HandlerMethod" || values.shellType === "HandlerFunction") &&
(values.urlPattern === "/*" || !values.urlPattern)
) {
throw new Error(t("tips.handlerUrlPattern"));
}
if (values.shellTool === ShellToolType.Custom && !values.shellClassBase64) {
throw new Error(t("tips.customShellClass"));
}
}
export function transformToPostData(formValue: FormSchema) { export function transformToPostData(formValue: FormSchema) {
const shellConfig: ShellConfig = { const shellConfig: ShellConfig = {