mirror of
https://github.com/ReaJason/MemShellParty.git
synced 2026-09-24 07:51:52 +08:00
refactor: use yup custom validator
This commit is contained in:
Binary file not shown.
+7
-7
@@ -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
@@ -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")}
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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
@@ -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,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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user