diff --git a/web/bun.lockb b/web/bun.lockb index 23b19a21..9b9571c9 100755 Binary files a/web/bun.lockb and b/web/bun.lockb differ diff --git a/web/package.json b/web/package.json index 2aaf5b0c..8f2ab5d1 100644 --- a/web/package.json +++ b/web/package.json @@ -20,7 +20,7 @@ "@types/react-copy-to-clipboard": "^5.0.7", "@types/react-dom": "^19.1.5", "@types/react-syntax-highlighter": "^15.5.13", - "@vitejs/plugin-react": "^4.4.1", + "@vitejs/plugin-react": "^4.5.0", "rimraf": "^6.0.1", "tailwindcss": "^4.1.7", "typescript": "^5.8.3", @@ -56,13 +56,13 @@ "@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.7", "@tailwindcss/vite": "^4.1.7", - "@tanstack/react-query": "^5.76.1", + "@tanstack/react-query": "^5.77.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", - "i18next": "^25.2.0", + "i18next": "^25.2.1", "input-otp": "^1.4.2", "lucide-react": "^0.511.0", "react": "^19.1.0", @@ -70,10 +70,10 @@ "react-day-picker": "9.7.0", "react-dom": "^19.1.0", "react-hook-form": "^7.56.4", - "react-i18next": "^15.5.1", + "react-i18next": "^15.5.2", "react-resizable-panels": "^3.0.2", - "react-router": "^7.6.0", - "react-router-dom": "^7.6.0", + "react-router": "^7.6.1", + "react-router-dom": "^7.6.1", "react-syntax-highlighter": "^15.6.1", "recharts": "^2.15.3", "sonner": "^2.0.3", @@ -81,7 +81,7 @@ "tailwind-scrollbar": "^4.0.2", "tailwindcss-animate": "^1.0.7", "vaul": "^1.1.2", - "zod": "^3.25.20" + "yup": "^1.6.1" }, "trustedDependencies": ["@biomejs/biome"] } diff --git a/web/src/config.ts b/web/src/config.ts index 88ac4e44..ef7fc1b1 100644 --- a/web/src/config.ts +++ b/web/src/config.ts @@ -1,11 +1,51 @@ -import * as z from "zod"; +import * as yup from "yup"; -const EnvSchema = z.object({ - API_URL: z.optional(z.string()), - BASE_PATH: z.optional(z.string()), - MODE: z.string(), +const EnvSchema = yup.object({ + API_URL: yup.string().optional(), + BASE_PATH: yup.string().optional(), + MODE: yup.string().required(), }); +type EnvSchema = yup.InferType; +function safeParseYup(schema: yup.ObjectSchema, 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 = () => { // @ts-ignore const envVars = Object.entries(import.meta.env).reduce>((acc, curr) => { @@ -20,14 +60,15 @@ const createEnv = () => { } return acc; }, {}); - const parsedEnv = EnvSchema.safeParse(envVars); + console.log(envVars) + const parsedEnv = safeParseYup(EnvSchema, envVars); if (!parsedEnv.success) { throw new Error( `Invalid env provided. The following variables are missing or invalid: -${Object.entries(parsedEnv.error.flatten().fieldErrors) - .map(([k, v]) => `- ${k}: ${v}`) - .join("\n")} +${parsedEnv.error.issues + .map(({ path, message }) => `- ${path}: ${message}`) + .join("\n")} `, ); } diff --git a/web/src/i18n/en.json b/web/src/i18n/en.json index 95a86d43..790aa8a0 100644 --- a/web/src/i18n/en.json +++ b/web/src/i18n/en.json @@ -130,6 +130,7 @@ "agent-move-to-target": "Move MemShellAgent.jar and jattach 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", + "specificUrlPattern": "URL Pattern must be specified, e.g., /hello", "shellBytesEmpty": "Shell bytes is empty, please generate shell first", "shellToolNotSelected": "Please select a shell tool type first", "targetServerNotFound": "Target server not found?", @@ -143,5 +144,6 @@ "updateAvailableTooltip": "Click to Open Github Release Page ( v{{currentVersion}} -> v{{latestVersion}})" }, "generator": "Generator", - "about": "About" + "about": "About", + "classNameOptions": "classNameOptions" } diff --git a/web/src/i18n/zh-CN.json b/web/src/i18n/zh-CN.json index a147c849..dd3af3a5 100644 --- a/web/src/i18n/zh-CN.json +++ b/web/src/i18n/zh-CN.json @@ -136,12 +136,14 @@ "targetServerRequest": "请求适配", "try-to-use-shell": "尝试利用内存马", "waitingForGeneration": "// 等待填写参数生成中...", - "customShellClass": "请输入自定义内存马类,base64 或类文件" + "customShellClass": "请输入自定义内存马类,base64 或类文件", + "specificUrlPattern": "必须指定 URL Pattern,例如 /hello" }, "version": { "updateAvailable": "有可用升级", "updateAvailableTooltip": "点击前往 GitHub Release ( v{{currentVersion}} -> v{{latestVersion}})" }, "about": "关于", - "generator": "生成器" + "generator": "生成器", + "classNameOptions": "类名配置项" } diff --git a/web/src/pages/index.tsx b/web/src/pages/index.tsx index f1770e9b..dc66e56f 100644 --- a/web/src/pages/index.tsx +++ b/web/src/pages/index.tsx @@ -4,7 +4,7 @@ import { ShellResult } from "@/components/shell-result.tsx"; import { Button } from "@/components/ui/button"; import { Form } from "@/components/ui/form.tsx"; import { env } from "@/config.ts"; -import { FormSchema, formSchema } from "@/types/schema.ts"; +import { FormSchema, formSchema, useYupValidationResolver } from "@/types/schema.ts"; import { APIErrorResponse, GenerateResponse, @@ -14,8 +14,7 @@ import { ServerConfig, ShellToolType, } from "@/types/shell.ts"; -import { customValidation, transformToPostData } from "@/utils/transformer.ts"; -import { zodResolver } from "@hookform/resolvers/zod"; +import { transformToPostData } from "@/utils/transformer.ts"; import { useQuery } from "@tanstack/react-query"; import { LoaderCircle, WandSparklesIcon } from "lucide-react"; import { useState, useTransition } from "react"; @@ -52,8 +51,8 @@ export default function IndexPage() { }); const { t } = useTranslation(); - const form = useForm({ - resolver: zodResolver(formSchema), + const form = useForm({ + resolver: useYupValidationResolver(formSchema, t), defaultValues: { server: urlParams.server ?? "Tomcat", targetJdkVersion: urlParams.targetJdkVersion ?? "50", @@ -86,7 +85,6 @@ export default function IndexPage() { const onSubmit = async (data: FormSchema) => { startTransition(async () => { try { - customValidation(t, data); const postData = transformToPostData(data); const response = await fetch(`${env.API_URL}/generate`, { method: "POST", diff --git a/web/src/types/schema.ts b/web/src/types/schema.ts index 3d31145b..cc80030e 100644 --- a/web/src/types/schema.ts +++ b/web/src/types/schema.ts @@ -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({ - server: z.string().min(1), - targetJdkVersion: z.optional(z.string()), - debug: z.optional(z.boolean()), - bypassJavaModule: z.optional(z.boolean()), - shellClassName: z.string().optional(), - shellTool: z.string().min(1), - shellType: z.string().min(1), - urlPattern: z.optional(z.string()), - godzillaPass: z.optional(z.string()), - godzillaKey: z.optional(z.string()), - behinderPass: z.optional(z.string()), - antSwordPass: z.optional(z.string()), - commandParamName: z.optional(z.string()), - implementationClass: z.optional(z.string()), - headerName: z.optional(z.string()), - headerValue: z.optional(z.string()), - injectorClassName: z.optional(z.string()), - packingMethod: z.string().min(1), - shrink: z.optional(z.boolean()), - shellClassBase64: z.optional(z.string()), - encryptor: z.optional(z.string()), +export const formSchema = yup.object({ + server: yup.string().required().min(1), + targetJdkVersion: yup.string().optional(), + debug: yup.boolean().optional(), + bypassJavaModule: yup.boolean().optional(), + shellClassName: yup.string().optional(), + shellTool: yup.string().required().min(1), + shellType: yup.string().required().min(1), + urlPattern: yup.string().optional(), + godzillaPass: yup.string().optional(), + godzillaKey: yup.string().optional(), + behinderPass: yup.string().optional(), + antSwordPass: yup.string().optional(), + commandParamName: yup.string().optional(), + implementationClass: yup.string().optional(), + headerName: yup.string().optional(), + headerValue: yup.string().optional(), + injectorClassName: yup.string().optional(), + packingMethod: yup.string().required().min(1), + shrink: yup.boolean().optional(), + shellClassBase64: yup.string().optional(), + encryptor: yup.string().optional(), }); -export type FormSchema = z.infer; +interface ValidationResult { + values: FormSchema; + errors: FieldErrors; +} + +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, t: TFunction) => + useCallback( + async (data: FormSchema): Promise => { + 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: + ...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; diff --git a/web/src/utils/transformer.ts b/web/src/utils/transformer.ts index fa595857..e073c960 100644 --- a/web/src/utils/transformer.ts +++ b/web/src/utils/transformer.ts @@ -1,27 +1,5 @@ import { FormSchema } from "@/types/schema.ts"; -import { InjectorConfig, ShellConfig, ShellToolConfig, ShellToolType } 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")); - } -} +import { InjectorConfig, ShellConfig, ShellToolConfig } from "@/types/shell.ts"; export function transformToPostData(formValue: FormSchema) { const shellConfig: ShellConfig = {