diff --git a/.gitmodules b/.gitmodules index b04daa8..7986b36 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "wasm/libcurl-wasm"] path = wasm/libcurl-wasm url = https://github.com/lbr77/libcurl.js +[submodule "wasm/openssl-wasm"] + path = wasm/openssl-wasm + url = https://github.com/jedisct1/openssl-wasm diff --git a/README.md b/README.md index 102089b..55d6577 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,13 @@ -# sideload.js +# Sideload.js -This repository is organized as a Bun workspace with four top-level areas: - -- `frontend/`: browser signing UI -- `backend/`: Cloudflare Workers Wisp proxy demo -- `dependencies/webmuxd/`: the publishable `webmuxd` package consumed by the frontend -- `wasm/openssl/`: the Rust/WASM OpenSSL bridge whose build artifacts are copied into `dependencies/webmuxd/lib/openssl-wasm` +A pure frontend signing infrastructure. ## Install -```bash -bun install -``` -## Validate + +Deploy to Cloudflare worker. -```bash -bun run build -bun run lint -bun run test -cd frontend && bun run build -cd backend && bun run check -``` +## Technology -## Workspace Notes - -- `frontend/src/main.ts` must consume `webmuxd` package exports instead of duplicating usbmux/lockdown/AFC/InstProxy logic. -- Changes to device communication, pairing, or TLS behavior belong in `dependencies/webmuxd/src/` first. -- `wasm/openssl/pkg` is treated as a build artifact source for the package copy step. +1. when signing \ No newline at end of file diff --git a/README.zh.md b/README.zh.md new file mode 100644 index 0000000..e21e362 --- /dev/null +++ b/README.zh.md @@ -0,0 +1,29 @@ +# Sideload.js + +纯前端ipa签名工具 + +## 安装/部署 + + + +Deploy to Cloudflare worker. + + +本地部署(With Dockerfile) + + +## 功能 + +1. apple客户端模拟(by unicorn over wasm) +2. Team,证书管理(获取,删除,注册) +3. zsign wasm(ipa解包,签名) +4. usbmuxd over webusb + lockdownd SSL service握手 + + + +## 感谢 + +1. libimobiledevice +2. https://github.com/hack-different/webmuxd +3. zsign +4. openssl-wasm \ No newline at end of file diff --git a/bun.lock b/bun.lock index 37965ab..91d9f80 100644 --- a/bun.lock +++ b/bun.lock @@ -41,10 +41,11 @@ "version": "0.0.0", "dependencies": { "@lbr77/anisette-js": "0.1.3", - "@lbr77/zsign-wasm-resigner-wrapper": "^0.1.5", + "@lbr77/zsign-wasm-resigner-wrapper": "workspace:*", "altsign.js": "^0.1.2", "fflate": "^0.8.2", "jszip": "^3.10.1", + "libcurl.js": "workspace:*", "node-forge": "^1.3.3", "webmuxd": "workspace:*", }, @@ -55,6 +56,25 @@ "vite": "^7.3.1", }, }, + "wasm/libcurl-wasm": { + "name": "libcurl.js", + "version": "0.7.4", + }, + "wasm/openssl": { + "name": "@lbr77/openssl-wasm", + "version": "0.1.0", + }, + "wasm/zsign-wasm": { + "name": "@lbr77/zsign-wasm-resigner-wrapper", + "version": "0.1.0", + "dependencies": { + "jszip": "^3.10.1", + }, + "devDependencies": { + "@types/node": "^25.5.2", + "typescript": "~5.9.3", + }, + }, }, "packages": { "@babel/code-frame": ["@babel/code-frame@7.16.0", "", { "dependencies": { "@babel/highlight": "^7.16.0" } }, "sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA=="], @@ -295,7 +315,9 @@ "@lbr77/anisette-js": ["@lbr77/anisette-js@0.1.3", "", { "dependencies": { "@types/node": "^25.3.2" } }, "sha512-QzZQB/XzMV68ykaj6yU2aPGmDrhE0wJEmSfMJu0VQ5zhfhrv7G2zrHJGUum226jZ//lYJeMSrzmDaI/NgFVIgg=="], - "@lbr77/zsign-wasm-resigner-wrapper": ["@lbr77/zsign-wasm-resigner-wrapper@0.1.5", "", { "dependencies": { "@types/node": "^25.3.2", "jszip": "^3.10.1" } }, "sha512-oVmRgkrSccDXpS9rlBKc0LWQzqcEFiTQod5ZaKIcyIp3rtwROgc1Tqwrr/7oLVQNdq8jI79lbnCoa1J34AaUqw=="], + "@lbr77/openssl-wasm": ["@lbr77/openssl-wasm@workspace:wasm/openssl"], + + "@lbr77/zsign-wasm-resigner-wrapper": ["@lbr77/zsign-wasm-resigner-wrapper@workspace:wasm/zsign-wasm"], "@mercuryworkshop/wisp-js": ["@mercuryworkshop/wisp-js@0.4.1", "", { "dependencies": { "bufferutil": "^4.0.9", "commander": "^14.0.2", "ipaddr.js": "^2.3.0", "ws": "^8.18.3" }, "bin": { "wisp-js-server": "src/bin/server_cli.mjs" } }, "sha512-104LwiXiuhti/e32gmv0Da0u0yuLFDHX8JawCzleTPWJ5t5qTX4EYi4E7/ucbjBPN9wwVPWHE5g5yGqzl/NzQA=="], @@ -923,6 +945,8 @@ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "libcurl.js": ["libcurl.js@workspace:wasm/libcurl-wasm"], + "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -1411,6 +1435,8 @@ "acorn-globals/acorn": ["acorn@7.4.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="], + "altsign.js/@lbr77/zsign-wasm-resigner-wrapper": ["@lbr77/zsign-wasm-resigner-wrapper@0.1.5", "", { "dependencies": { "@types/node": "^25.3.2", "jszip": "^3.10.1" } }, "sha512-oVmRgkrSccDXpS9rlBKc0LWQzqcEFiTQod5ZaKIcyIp3rtwROgc1Tqwrr/7oLVQNdq8jI79lbnCoa1J34AaUqw=="], + "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "anymatch/picomatch": ["picomatch@2.3.0", "", {}, "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw=="], diff --git a/dependencies/webmuxd/package.json b/dependencies/webmuxd/package.json index d9df79b..7829075 100644 --- a/dependencies/webmuxd/package.json +++ b/dependencies/webmuxd/package.json @@ -25,7 +25,8 @@ "LICENSE" ], "scripts": { - "build": "tsc && bun run copy:openssl-wasm", + "build": "bun run build:openssl-wasm && tsc && bun run copy:openssl-wasm", + "build:openssl-wasm": "cd ../../wasm/openssl && bun run build", "copy:openssl-wasm": "bun scripts/copy-openssl-wasm.mjs", "prepare": "bun run build", "prepublishOnly": "bun run test && bun run lint", diff --git a/dependencies/webmuxd/scripts/copy-openssl-wasm.mjs b/dependencies/webmuxd/scripts/copy-openssl-wasm.mjs index a10b7ce..833ebe2 100644 --- a/dependencies/webmuxd/scripts/copy-openssl-wasm.mjs +++ b/dependencies/webmuxd/scripts/copy-openssl-wasm.mjs @@ -1,22 +1,19 @@ -import { mkdir, copyFile } from "node:fs/promises" +import { cp, mkdir, rm } from "node:fs/promises" import { dirname, resolve } from "node:path" import { fileURLToPath } from "node:url" const scriptDir = dirname(fileURLToPath(import.meta.url)) const packageDir = resolve(scriptDir, "..") const workspaceRootDir = resolve(packageDir, "../..") -const sourceDir = resolve(workspaceRootDir, "wasm/openssl/pkg") +const sourceDir = resolve(workspaceRootDir, "wasm/openssl") const targetDir = resolve(packageDir, "lib/openssl-wasm") +const directoriesToCopy = ["dist", "binary"] -const filesToCopy = [ - "openssl_wasm.js", - "openssl_wasm.d.ts", - "openssl_wasm_bg.wasm", - "openssl_wasm_bg.wasm.d.ts", -] - +await rm(targetDir, { recursive: true, force: true }) await mkdir(targetDir, { recursive: true }) -for (const fileName of filesToCopy) { - await copyFile(resolve(sourceDir, fileName), resolve(targetDir, fileName)) +for (const directoryName of directoriesToCopy) { + await cp(resolve(sourceDir, directoryName), resolve(targetDir, directoryName), { + recursive: true, + }) } diff --git a/dependencies/webmuxd/src/browser.ts b/dependencies/webmuxd/src/browser.ts new file mode 100644 index 0000000..02cc542 --- /dev/null +++ b/dependencies/webmuxd/src/browser.ts @@ -0,0 +1,19 @@ +export { CONSOLE_LOGGER, NULL_LOGGER, type Logger } from "./logger" +export { WebUsbTransport, type WebUsbTransportOptions } from "./core/webusb-transport" +export { + DirectUsbMuxClient, + LOCKDOWN_PORT, + installIpaViaInstProxy, + sanitizeIpaFileName, + createHostId, + createSystemBuid, + encodeStoredPairRecord, + decodeStoredPairRecord, + type PairRecord, + type StoredPairRecordPayload, + type WebUsbTransportInstance, +} from "./core/imobiledevice-client" +export { + createOpenSslWasmTlsFactory, + generatePairRecordWithOpenSslWasm, +} from "./core/openssl-wasm-browser" diff --git a/dependencies/webmuxd/src/core/openssl-wasm-browser.ts b/dependencies/webmuxd/src/core/openssl-wasm-browser.ts new file mode 100644 index 0000000..8e7bd11 --- /dev/null +++ b/dependencies/webmuxd/src/core/openssl-wasm-browser.ts @@ -0,0 +1,112 @@ +import type { TlsConnection, TlsConnectionFactory } from "./imobiledevice-client" +import type { + OpenSslWasmConnectionRequest, + OpenSslWasmPairRecordRequest, +} from "./openssl-wasm" + +interface OpenSslWasmBrowserModule { + default(input?: unknown): Promise + OpensslClient: new ( + serverName: string, + caCertificatePem: string, + certificatePem: string, + privateKeyPem: string, + ) => TlsConnection + libimobiledevice_generate_pair_record( + devicePublicKey: Uint8Array, + hostId: string, + systemBuid: string, + ): string +} + +const OPENSSL_WASM_MODULE_URL = new URL( + "../openssl-wasm/dist/index.mjs", + import.meta.url, +) + +let opensslWasmModule: OpenSslWasmBrowserModule | null = null +let opensslWasmModulePromise: Promise | null = null +let opensslWasmInitPromise: Promise | null = null + +const toOpenSslWasmModule = (moduleValue: unknown): OpenSslWasmBrowserModule => { + if (!moduleValue || typeof moduleValue !== "object") { + throw new Error("OpenSSL wasm module did not return an object") + } + + const candidate = moduleValue as Record + if (typeof candidate.default !== "function") { + throw new Error("OpenSSL wasm module is missing its default initializer") + } + if (typeof candidate.OpensslClient !== "function") { + throw new Error("OpenSSL wasm module is missing OpensslClient") + } + if (typeof candidate.libimobiledevice_generate_pair_record !== "function") { + throw new Error("OpenSSL wasm module is missing pair record generation") + } + + return candidate as unknown as OpenSslWasmBrowserModule +} + +const loadOpenSslWasmModule = async (): Promise => { + if (!opensslWasmModulePromise) { + opensslWasmModulePromise = import( + /* @vite-ignore */ + OPENSSL_WASM_MODULE_URL.href + ).then((moduleValue) => { + const loadedModule = toOpenSslWasmModule(moduleValue) + opensslWasmModule = loadedModule + return loadedModule + }) + } + + return await opensslWasmModulePromise +} + +const requireOpenSslWasmModule = (): OpenSslWasmBrowserModule => { + if (!opensslWasmModule) { + throw new Error("OpenSSL wasm is not ready. Call ensureOpenSslWasmReady() first.") + } + + return opensslWasmModule +} + +export const ensureOpenSslWasmReady = async (): Promise => { + if (!opensslWasmInitPromise) { + opensslWasmInitPromise = loadOpenSslWasmModule().then(async (moduleValue) => { + await moduleValue.default() + }) + } + + await opensslWasmInitPromise +} + +export const createOpenSslWasmConnection = ( + request: OpenSslWasmConnectionRequest, +): TlsConnection => { + const moduleValue = requireOpenSslWasmModule() + return new moduleValue.OpensslClient( + request.serverName, + request.caCertificatePem, + request.certificatePem, + request.privateKeyPem, + ) +} + +export const createOpenSslWasmTlsFactory = (): TlsConnectionFactory => { + return { + ensureReady: ensureOpenSslWasmReady, + createConnection: createOpenSslWasmConnection, + } +} + +export const generatePairRecordWithOpenSslWasm = async ( + request: OpenSslWasmPairRecordRequest, +): Promise => { + await ensureOpenSslWasmReady() + const moduleValue = requireOpenSslWasmModule() + return moduleValue.libimobiledevice_generate_pair_record( + new Uint8Array(request.devicePublicKey), + request.hostId, + request.systemBuid, + ) +} diff --git a/dependencies/webmuxd/src/core/openssl-wasm.ts b/dependencies/webmuxd/src/core/openssl-wasm.ts index 0ad1bb6..3fd3971 100644 --- a/dependencies/webmuxd/src/core/openssl-wasm.ts +++ b/dependencies/webmuxd/src/core/openssl-wasm.ts @@ -28,7 +28,7 @@ interface OpenSslWasmModule { ): string } -const OPENSSL_WASM_MODULE_SPECIFIER = "../openssl-wasm/openssl_wasm.js" +const OPENSSL_WASM_MODULE_SPECIFIER = "../openssl-wasm/dist/index.mjs" /** * Keep native `import()` intact in the CommonJS build so bundlers can defer the diff --git a/dependencies/webmuxd/src/core/webusb-transport.ts b/dependencies/webmuxd/src/core/webusb-transport.ts index 11e2075..6be3ba4 100644 --- a/dependencies/webmuxd/src/core/webusb-transport.ts +++ b/dependencies/webmuxd/src/core/webusb-transport.ts @@ -1,4 +1,4 @@ -import { CONSOLE_LOGGER, Logger, NULL_LOGGER } from "../webmuxd" +import { CONSOLE_LOGGER, Logger, NULL_LOGGER } from "../logger" import { DataHandler, DisconnectHandler, UsbMuxTransport } from "./transport" const USBMUX_CLASS = 255 diff --git a/dependencies/webmuxd/src/logger.ts b/dependencies/webmuxd/src/logger.ts new file mode 100644 index 0000000..9d86f37 --- /dev/null +++ b/dependencies/webmuxd/src/logger.ts @@ -0,0 +1,38 @@ +type LogLevel = "debug" | "info" | "warn" | "error" + +export interface Logger { + log(level: LogLevel, message: string): void +} + +export const NULL_LOGGER = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + log: (level: LogLevel, message: string): void => { + return + }, +} + +export const CONSOLE_LOGGER = { + log: (level: LogLevel, message: string): void => { + switch (level) { + case "info": + // eslint-disable-next-line no-console + console.log(message) + break + case "warn": + // eslint-disable-next-line no-console + console.warn(message) + break + case "error": + // eslint-disable-next-line no-console + console.error(message) + break + case "debug": + // eslint-disable-next-line no-console + console.debug(message) + break + default: + // eslint-disable-next-line no-console + console.error(`Unknown log level ${level}: ${message}`) + } + }, +} diff --git a/dependencies/webmuxd/src/openssl-wasm/dist/index.mjs b/dependencies/webmuxd/src/openssl-wasm/dist/index.mjs new file mode 100644 index 0000000..bb03a94 --- /dev/null +++ b/dependencies/webmuxd/src/openssl-wasm/dist/index.mjs @@ -0,0 +1,2 @@ +export { default } from "../../../../../wasm/openssl/dist/index.mjs" +export * from "../../../../../wasm/openssl/dist/index.mjs" diff --git a/dependencies/webmuxd/src/webmuxd.ts b/dependencies/webmuxd/src/webmuxd.ts index 8652bb4..dad2603 100644 --- a/dependencies/webmuxd/src/webmuxd.ts +++ b/dependencies/webmuxd/src/webmuxd.ts @@ -1,47 +1,11 @@ +import { NULL_LOGGER, type Logger } from "./logger" const USBMUX_USB_FILTER = [{ vendorId: 0x5ac, productId: 0x12a8 }]; const USBMUX_CLASS = 255; const USBMUX_SUBCLASS = 254; const USBMUX_PROTOCOL = 2; -type LogLevel = "debug" | "info" | "warn" | "error" - -export interface Logger { - log(level: LogLevel, message: string): void -} - -export const NULL_LOGGER = { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - log: (level: LogLevel, message: string): void => { - return - } -} - -export const CONSOLE_LOGGER = { - log: (level: LogLevel, message: string): void => { - switch (level) { - case "info": - // eslint-disable-next-line no-console - console.log(message) - break - case "warn": - // eslint-disable-next-line no-console - console.warn(message) - break - case "error": - // eslint-disable-next-line no-console - console.error(message) - break - case "debug": - // eslint-disable-next-line no-console - console.debug(message) - break - default: - // eslint-disable-next-line no-console - console.error(`Unknown log level ${level}: ${message}`) - } - } -} +export { CONSOLE_LOGGER, NULL_LOGGER, type Logger } from "./logger" export default class MobileDevice { static logger: Logger = NULL_LOGGER diff --git a/dependencies/webmuxd/tsconfig.json b/dependencies/webmuxd/tsconfig.json index 20c3e75..bc5546b 100644 --- a/dependencies/webmuxd/tsconfig.json +++ b/dependencies/webmuxd/tsconfig.json @@ -9,5 +9,11 @@ "types": ["node", "w3c-web-usb"] }, "include": ["src"], - "exclude": ["node_modules", "**/__tests__/*"] + "exclude": [ + "node_modules", + "**/__tests__/*", + "src/browser.ts", + "src/core/openssl-wasm-browser.ts", + "src/openssl-wasm/**/*" + ] } diff --git a/frontend/package.json b/frontend/package.json index d930167..1bec710 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,10 +11,11 @@ }, "dependencies": { "@lbr77/anisette-js": "0.1.3", - "@lbr77/zsign-wasm-resigner-wrapper": "^0.1.5", + "@lbr77/zsign-wasm-resigner-wrapper": "workspace:*", "altsign.js": "^0.1.2", "fflate": "^0.8.2", "jszip": "^3.10.1", + "libcurl.js": "workspace:*", "node-forge": "^1.3.3", "webmuxd": "workspace:*" }, diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/frontend/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/anisette-libcurl-http.ts b/frontend/src/anisette-libcurl-http.ts index ad2e085..cda0092 100644 --- a/frontend/src/anisette-libcurl-http.ts +++ b/frontend/src/anisette-libcurl-http.ts @@ -1,9 +1,11 @@ import type { HttpClient } from "@lbr77/anisette-js" -import { initLibcurl, libcurl } from "./anisette-libcurl-init" +import { initLibcurl } from "./anisette-libcurl-init" +import { requireLibcurl } from "./wasm/libcurl" export class LibcurlHttpClient implements HttpClient { async get(url: string, headers: Record): Promise { await initLibcurl() + const libcurl = requireLibcurl() const response = await libcurl.fetch(url, { method: "GET", @@ -21,6 +23,7 @@ export class LibcurlHttpClient implements HttpClient { async post(url: string, body: string, headers: Record): Promise { await initLibcurl() + const libcurl = requireLibcurl() const response = await libcurl.fetch(url, { method: "POST", diff --git a/frontend/src/anisette-libcurl-init.ts b/frontend/src/anisette-libcurl-init.ts index 5173441..3fd8e8b 100644 --- a/frontend/src/anisette-libcurl-init.ts +++ b/frontend/src/anisette-libcurl-init.ts @@ -1,5 +1,4 @@ -// @ts-ignore -import { libcurl } from "../public/anisette/libcurl_full.mjs" +import { loadLibcurl, libcurl } from "./wasm/libcurl" let initialized = false let initPromise: Promise | null = null @@ -13,10 +12,11 @@ export async function initLibcurl(): Promise { } initPromise = (async () => { + const loadedLibcurl = await loadLibcurl() const wsProto = location.protocol === "https:" ? "wss:" : "ws:" const wsUrl = `${wsProto}//${location.host}/wisp/` - libcurl.set_websocket(wsUrl) - await libcurl.load_wasm() + loadedLibcurl.set_websocket(wsUrl) + await loadedLibcurl.load_wasm() initialized = true })() diff --git a/frontend/src/apple-signing.ts b/frontend/src/apple-signing.ts index d44f323..f9bb164 100644 --- a/frontend/src/apple-signing.ts +++ b/frontend/src/apple-signing.ts @@ -1,13 +1,14 @@ import { strFromU8, unzipSync } from "fflate" -import { - type AnisetteData, - type AppleAPI, - type AppID, - type Certificate, - type Device, - type Team, +import type { + AnisetteData, + AppleAPI, + AppID, + Certificate, + Device, + Team, } from "altsign.js" -import { initLibcurl, libcurl } from "./anisette-libcurl-init" +import { initLibcurl } from "./anisette-libcurl-init" +import { requireLibcurl } from "./wasm/libcurl" const SIGNING_IDENTITY_STORAGE_KEY = "webmuxd:signing-identities" const PRIMARY_APP_INFO_PLIST_RE = /^Payload\/[^/]+\.app\/Info\.plist$/ @@ -129,6 +130,7 @@ async function getAppleApi(): Promise { } const { AppleAPI, Fetch } = await loadAltsignModule() const appleFetch = new Fetch(initLibcurl, async (url, options) => { + const libcurl = requireLibcurl() const response = await libcurl.fetch(url, { method: options.method, headers: options.headers, diff --git a/frontend/src/counter.ts b/frontend/src/counter.ts deleted file mode 100644 index 09e5afd..0000000 --- a/frontend/src/counter.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function setupCounter(element: HTMLButtonElement) { - let counter = 0 - const setCounter = (count: number) => { - counter = count - element.innerHTML = `count is ${counter}` - } - element.addEventListener('click', () => setCounter(counter + 1)) - setCounter(0) -} diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 079d3c0..b11d95d 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,121 +1,35 @@ import "./style.css" -import * as webmuxdModule from "webmuxd" import { - initAnisette, - getAnisetteData, - provisionAnisette, - type AnisetteData, -} from "./anisette-service" -import { - loginAppleDeveloperAccount, - refreshAppleDeveloperContext, - signIpaWithAppleContext, - type AppleDeveloperContext, -} from "./apple-signing" + DirectUsbMuxClient, + LOCKDOWN_PORT, + WebUsbTransport, + createHostId, + createOpenSslWasmTlsFactory, + createSystemBuid, + decodeStoredPairRecord, + encodeStoredPairRecord, + generatePairRecordWithOpenSslWasm, + installIpaViaInstProxy, + sanitizeIpaFileName, + type PairRecord, + type StoredPairRecordPayload, +} from "webmuxd" +import type { AnisetteData } from "./anisette-service" +import type { AppleDeveloperContext } from "./apple-signing" -interface WebUsbTransportInstance { - readonly isOpen: boolean - open(): Promise - close(): Promise - send(data: ArrayBuffer): Promise - setDataHandler(handler: ((data: ArrayBuffer) => void) | null): void - setDisconnectHandler(handler: ((reason?: unknown) => void) | null): void -} +type WasmPairRecordPayload = Pick< + PairRecord, + | "hostId" + | "systemBuid" + | "hostCertificatePem" + | "hostPrivateKeyPem" + | "rootCertificatePem" + | "rootPrivateKeyPem" + | "deviceCertificatePem" +> -interface WebUsbTransportCtor { - supported(): boolean - requestAppleDevice(): Promise -} - -interface PairRecord { - hostId: string - systemBuid: string - hostCertificatePem: string - hostPrivateKeyPem: string - rootCertificatePem: string - rootPrivateKeyPem: string - deviceCertificatePem: string - devicePublicKey: Uint8Array - escrowBag?: Uint8Array -} - -interface StoredPairRecordPayload { - hostId: string - systemBuid: string - hostCertificatePem: string - hostPrivateKeyPem: string - rootCertificatePem: string - rootPrivateKeyPem: string - deviceCertificatePem: string - devicePublicKey: string - escrowBag: string | null -} - -interface StartSessionResult { - sessionId: string - enableSessionSsl: boolean -} - -interface DirectUsbMuxClient { - readonly isHandshakeComplete: boolean - readonly isLockdownConnected: boolean - readonly isSessionStarted: boolean - readonly isSessionSslEnabled: boolean - readonly isTlsActive: boolean - readonly isPaired: boolean - loadPairRecord(record: PairRecord | null): void - openAndHandshake(): Promise - connectLockdown(port?: number): Promise - getOrFetchDeviceUdid(): Promise - getOrFetchDeviceName(): Promise - pairDevice(hostId: string, systemBuid: string): Promise - startSession(hostId: string, systemBuid: string): Promise - close(): Promise -} - -interface DirectUsbMuxClientCtor { - new ( - transport: WebUsbTransportInstance, - options?: { - log?: (message: string) => void - onStateChange?: () => void - lockdownLabel?: string - tlsFactory?: { - ensureReady?: () => Promise - createConnection(request: { - serverName: string - caCertificatePem: string - certificatePem: string - privateKeyPem: string - }): { - is_handshaking(): boolean - write_plaintext(data: Uint8Array): void - feed_tls(data: Uint8Array): void - take_tls_out(): Uint8Array - take_plain_out(): Uint8Array - free(): void - } - } - pairRecordFactory?: { - createPairRecord(request: { - devicePublicKey: Uint8Array - hostId: string - systemBuid: string - }): Promise - } - }, - ): DirectUsbMuxClient -} - -interface WasmPairRecordPayload { - hostId: string - systemBuid: string - hostCertificatePem: string - hostPrivateKeyPem: string - rootCertificatePem: string - rootPrivateKeyPem: string - deviceCertificatePem: string -} +type AnisetteServiceModule = typeof import("./anisette-service") +type AppleSigningModule = typeof import("./apple-signing") interface PairedDeviceInfo { udid: string @@ -162,8 +76,6 @@ interface StoredAccountSessionPayload { type AppPage = "login" | "sign" -const LOCKDOWN_PORT = 62078 - const HOST_ID_STORAGE_KEY = "webmuxd:host-id" const SYSTEM_BUID_STORAGE_KEY = "webmuxd:system-buid" const PAIR_RECORDS_STORAGE_KEY = "webmuxd:pair-records-by-udid" @@ -177,67 +89,24 @@ const SELECTED_DEVICE_UDID_STORAGE_KEY = "webmuxd:selected-device-udid" const LOGIN_PAGE_HASH = "#/login" const SIGN_PAGE_HASH = "#/sign" -const webmuxdModuleValue = webmuxdModule as unknown as Record +let anisetteServicePromise: Promise | null = null +let appleSigningModulePromise: Promise | null = null -const WebUsbTransport = resolveWebmuxdExport( - webmuxdModuleValue, - "WebUsbTransport", -) -const WebmuxdDirectUsbMuxClient = resolveWebmuxdExport( - webmuxdModuleValue, - "DirectUsbMuxClient", -) -const webmuxdInstallIpaViaInstProxy = resolveWebmuxdExport< - ( - client: DirectUsbMuxClient, - ipaData: Uint8Array, - fileName: string, - onLog?: (message: string) => void, - ) => Promise ->(webmuxdModuleValue, "installIpaViaInstProxy") -const webmuxdSanitizeIpaFileName = resolveWebmuxdExport<(fileName: string) => string>( - webmuxdModuleValue, - "sanitizeIpaFileName", -) -const webmuxdCreateHostId = resolveWebmuxdExport<() => string>( - webmuxdModuleValue, - "createHostId", -) -const webmuxdCreateSystemBuid = resolveWebmuxdExport<() => string>( - webmuxdModuleValue, - "createSystemBuid", -) -const webmuxdEncodeStoredPairRecord = resolveWebmuxdExport< - (record: PairRecord) => StoredPairRecordPayload ->(webmuxdModuleValue, "encodeStoredPairRecord") -const webmuxdDecodeStoredPairRecord = resolveWebmuxdExport< - (payload: StoredPairRecordPayload) => PairRecord | null ->(webmuxdModuleValue, "decodeStoredPairRecord") -const webmuxdCreateOpenSslWasmTlsFactory = resolveWebmuxdExport< - () => { - ensureReady?(): Promise - createConnection(request: { - serverName: string - caCertificatePem: string - certificatePem: string - privateKeyPem: string - }): { - is_handshaking(): boolean - write_plaintext(data: Uint8Array): void - feed_tls(data: Uint8Array): void - take_tls_out(): Uint8Array - take_plain_out(): Uint8Array - free(): void - } +const loadAnisetteService = async (): Promise => { + if (!anisetteServicePromise) { + anisetteServicePromise = import("./anisette-service") } ->(webmuxdModuleValue, "createOpenSslWasmTlsFactory") -const webmuxdGeneratePairRecordWithOpenSslWasm = resolveWebmuxdExport< - (request: { - devicePublicKey: Uint8Array - hostId: string - systemBuid: string - }) => Promise ->(webmuxdModuleValue, "generatePairRecordWithOpenSslWasm") + + return await anisetteServicePromise +} + +const loadAppleSigningModule = async (): Promise => { + if (!appleSigningModulePromise) { + appleSigningModulePromise = import("./apple-signing") + } + + return await appleSigningModulePromise +} const app = document.querySelector("#app") if (!app) { @@ -586,11 +455,11 @@ const ensureClientSelected = async (): Promise => { } const transport = await WebUsbTransport.requestAppleDevice() - directClient = new WebmuxdDirectUsbMuxClient(transport, { + directClient = new DirectUsbMuxClient(transport, { log: addLog, onStateChange: refreshUi, lockdownLabel: "webmuxd.frontend", - tlsFactory: webmuxdCreateOpenSslWasmTlsFactory(), + tlsFactory: createOpenSslWasmTlsFactory(), pairRecordFactory: { createPairRecord: async (request) => { return await createPairRecord(request.devicePublicKey, request.hostId, request.systemBuid) @@ -681,19 +550,20 @@ const ensureAnisetteData = async (): Promise => { return anisetteData } - const anisette = await initAnisette() + const anisetteService = await loadAnisetteService() + const anisette = await anisetteService.initAnisette() const alreadyProvisioned = anisette.isProvisioned anisetteProvisioned = alreadyProvisioned if (alreadyProvisioned) { addLog("login: anisette already provisioned") } else { addLog("login: preparing anisette environment...") - await provisionAnisette() + await anisetteService.provisionAnisette() anisetteProvisioned = true addLog("login: anisette provisioned") } - anisetteData = await getAnisetteData() + anisetteData = await anisetteService.getAnisetteData() addLog(`login: anisette ready (${shortToken(anisetteData.machineID)})`) refreshUi() return anisetteData @@ -701,7 +571,8 @@ const ensureAnisetteData = async (): Promise => { const syncAnisetteProvisionedStatus = async (): Promise => { try { - const anisette = await initAnisette() + const anisetteService = await loadAnisetteService() + const anisette = await anisetteService.initAnisette() anisetteProvisioned = anisette.isProvisioned refreshUi() } catch (error) { @@ -727,7 +598,8 @@ const loginAndSignFlow = async (): Promise => { const anisette = await ensureAnisetteData() addLog("login: authenticating Apple account...") - const context = await loginAppleDeveloperAccount({ + const appleSigning = await loadAppleSigningModule() + const context = await appleSigning.loginAppleDeveloperAccount({ anisetteData: anisette, credentials: { appleId, password }, onLog: addLog, @@ -736,7 +608,7 @@ const loginAndSignFlow = async (): Promise => { }, }) - loginContext = await refreshAppleDeveloperContext(context, addLog) + loginContext = await appleSigning.refreshAppleDeveloperContext(context, addLog) accountContextMap.set(accountKey(loginContext.appleId, loginContext.team.identifier), loginContext) persistAccountSummary(loginContext) if (rememberSessionInput.checked) { @@ -781,7 +653,8 @@ const signSelectedIpa = async (): Promise => { }, } - const refreshed = await refreshAppleDeveloperContext(loginContext, addLog) + const appleSigning = await loadAppleSigningModule() + const refreshed = await appleSigning.refreshAppleDeveloperContext(loginContext, addLog) loginContext = refreshed accountContextMap.set(accountKey(refreshed.appleId, refreshed.team.identifier), refreshed) persistAccountSummary(refreshed) @@ -790,7 +663,7 @@ const signSelectedIpa = async (): Promise => { } addLog("sign: preparing ipa...") - const result = await signIpaWithAppleContext({ + const result = await appleSigning.signIpaWithAppleContext({ ipaFile: selectedIpaFile, context: refreshed, deviceUdid: targetUdid, @@ -868,8 +741,8 @@ const installFlow = async (): Promise => { addLog("install: uploading and installing...") const bytes = new Uint8Array(await upload.arrayBuffer()) - const safeName = webmuxdSanitizeIpaFileName(upload.name) - await webmuxdInstallIpaViaInstProxy(client, bytes, safeName, addLog) + const safeName = sanitizeIpaFileName(upload.name) + await installIpaViaInstProxy(client, bytes, safeName, addLog) addLog("install: complete") setInstallProgress(100, "complete") } catch (error) { @@ -1405,7 +1278,7 @@ function getOrCreateHostId(): string { if (existing && existing.trim().length > 0) { return existing } - const created = webmuxdCreateHostId() + const created = createHostId() saveText(HOST_ID_STORAGE_KEY, created) return created } @@ -1415,7 +1288,7 @@ function getOrCreateSystemBuid(): string { if (existing && existing.trim().length > 0) { return existing } - const created = webmuxdCreateSystemBuid() + const created = createSystemBuid() saveText(SYSTEM_BUID_STORAGE_KEY, created) return created } @@ -1446,7 +1319,7 @@ function savePairRecordForUdid(udid: string, record: PairRecord): void { return } const map = readPairRecordMap() - map[normalizedUdid] = webmuxdEncodeStoredPairRecord(record) + map[normalizedUdid] = encodeStoredPairRecord(record) writePairRecordMap(map) } @@ -1457,7 +1330,7 @@ function loadLegacyPairRecord(): PairRecord | null { } try { const parsed = JSON.parse(text) as StoredPairRecordPayload - return webmuxdDecodeStoredPairRecord(parsed) + return decodeStoredPairRecord(parsed) } catch { return null } @@ -1473,7 +1346,7 @@ function loadPairRecordForUdid(udid: string): PairRecord | null { const fromMap = map[normalizedUdid] if (fromMap) { try { - return webmuxdDecodeStoredPairRecord(fromMap) + return decodeStoredPairRecord(fromMap) } catch { return null } @@ -1492,7 +1365,7 @@ async function createPairRecord( hostId: string, systemBuid: string, ): Promise { - const payloadText = await webmuxdGeneratePairRecordWithOpenSslWasm({ + const payloadText = await generatePairRecordWithOpenSslWasm({ devicePublicKey: devicePublicKeyBytes, hostId, systemBuid, @@ -1763,20 +1636,3 @@ function formatError(error: unknown): string { } return String(error) } - -function resolveWebmuxdExport(moduleValue: Record, key: string): T { - const direct = moduleValue[key] - if (direct !== undefined) { - return direct as T - } - - const defaultValue = moduleValue.default - if (defaultValue && typeof defaultValue === "object") { - const fromDefault = (defaultValue as Record)[key] - if (fromDefault !== undefined) { - return fromDefault as T - } - } - - throw new Error(`webmuxd export ${key} is unavailable`) -} diff --git a/frontend/src/node-forge.d.ts b/frontend/src/node-forge.d.ts deleted file mode 100644 index a584be8..0000000 --- a/frontend/src/node-forge.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare module "node-forge" - diff --git a/frontend/src/typescript.svg b/frontend/src/typescript.svg deleted file mode 100644 index d91c910..0000000 --- a/frontend/src/typescript.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/wasm/libcurl-entry.d.ts b/frontend/src/wasm/libcurl-entry.d.ts new file mode 100644 index 0000000..4ab3a62 --- /dev/null +++ b/frontend/src/wasm/libcurl-entry.d.ts @@ -0,0 +1,6 @@ +export const libcurl: { + fetch(input: string | URL, init?: Record): Promise + load_wasm(url?: string): Promise + set_websocket(url: string): void + readonly ready?: boolean +} diff --git a/frontend/src/wasm/libcurl-entry.js b/frontend/src/wasm/libcurl-entry.js new file mode 100644 index 0000000..2f708e6 --- /dev/null +++ b/frontend/src/wasm/libcurl-entry.js @@ -0,0 +1 @@ +export { libcurl } from "libcurl.js/bundled" diff --git a/frontend/src/wasm/libcurl.ts b/frontend/src/wasm/libcurl.ts new file mode 100644 index 0000000..8ed4877 --- /dev/null +++ b/frontend/src/wasm/libcurl.ts @@ -0,0 +1,53 @@ +export interface LibcurlApi { + fetch(input: string | URL, init?: Record): Promise + load_wasm(url?: string): Promise + set_websocket(url: string): void + readonly ready?: boolean +} + +let libcurlModulePromise: Promise | null = null +export let libcurl: LibcurlApi + +const toLibcurl = (moduleValue: unknown): LibcurlApi => { + if (!moduleValue || typeof moduleValue !== "object") { + throw new Error("libcurl module did not return an object") + } + + const candidate = moduleValue as Record + if (!candidate.libcurl || typeof candidate.libcurl !== "object") { + throw new Error("libcurl module is missing the libcurl export") + } + + const loadedLibcurl = candidate.libcurl as Partial + if (typeof loadedLibcurl.fetch !== "function") { + throw new Error("libcurl export is missing fetch") + } + if (typeof loadedLibcurl.load_wasm !== "function") { + throw new Error("libcurl export is missing load_wasm") + } + if (typeof loadedLibcurl.set_websocket !== "function") { + throw new Error("libcurl export is missing set_websocket") + } + + return loadedLibcurl as LibcurlApi +} + +export const loadLibcurl = async (): Promise => { + if (!libcurlModulePromise) { + libcurlModulePromise = import("./libcurl-entry.js").then((moduleValue) => { + const loadedLibcurl = toLibcurl(moduleValue) + libcurl = loadedLibcurl + return loadedLibcurl + }) + } + + return await libcurlModulePromise +} + +export const requireLibcurl = (): LibcurlApi => { + if (!libcurl) { + throw new Error("libcurl is not ready. Call initLibcurl() first.") + } + + return libcurl +} diff --git a/frontend/src/wasm/openssl-webmuxd.js b/frontend/src/wasm/openssl-webmuxd.js new file mode 100644 index 0000000..b285063 --- /dev/null +++ b/frontend/src/wasm/openssl-webmuxd.js @@ -0,0 +1,75 @@ +let opensslWasmModule = null +let opensslWasmModulePromise = null +let opensslWasmInitPromise = null + +const loadOpenSslWasmModule = async () => { + if (!opensslWasmModulePromise) { + opensslWasmModulePromise = import("../../../wasm/openssl/dist/index.mjs").then( + (moduleValue) => { + if (!moduleValue || typeof moduleValue !== "object") { + throw new Error("OpenSSL wasm module did not return an object") + } + const candidate = moduleValue + if (typeof candidate.default !== "function") { + throw new Error("OpenSSL wasm module is missing its default initializer") + } + if (typeof candidate.OpensslClient !== "function") { + throw new Error("OpenSSL wasm module is missing OpensslClient") + } + if (typeof candidate.libimobiledevice_generate_pair_record !== "function") { + throw new Error("OpenSSL wasm module is missing pair record generation") + } + + opensslWasmModule = candidate + return candidate + }, + ) + } + + return await opensslWasmModulePromise +} + +const requireOpenSslWasmModule = () => { + if (!opensslWasmModule) { + throw new Error("OpenSSL wasm is not ready. Call ensureOpenSslWasmReady() first.") + } + + return opensslWasmModule +} + +export const ensureOpenSslWasmReady = async () => { + if (!opensslWasmInitPromise) { + opensslWasmInitPromise = loadOpenSslWasmModule().then(async (moduleValue) => { + await moduleValue.default() + }) + } + + await opensslWasmInitPromise +} + +export const createOpenSslWasmConnection = (request) => { + const moduleValue = requireOpenSslWasmModule() + return new moduleValue.OpensslClient( + request.serverName, + request.caCertificatePem, + request.certificatePem, + request.privateKeyPem, + ) +} + +export const createOpenSslWasmTlsFactory = () => { + return { + ensureReady: ensureOpenSslWasmReady, + createConnection: createOpenSslWasmConnection, + } +} + +export const generatePairRecordWithOpenSslWasm = async (request) => { + await ensureOpenSslWasmReady() + const moduleValue = requireOpenSslWasmModule() + return moduleValue.libimobiledevice_generate_pair_record( + new Uint8Array(request.devicePublicKey), + request.hostId, + request.systemBuid, + ) +} diff --git a/frontend/src/webmuxd-browser.d.ts b/frontend/src/webmuxd-browser.d.ts new file mode 100644 index 0000000..648744b --- /dev/null +++ b/frontend/src/webmuxd-browser.d.ts @@ -0,0 +1,120 @@ +declare module "webmuxd" { + export interface WebUsbTransportInstance { + readonly isOpen: boolean + open(): Promise + close(): Promise + send(data: ArrayBuffer): Promise + setDataHandler(handler: ((data: ArrayBuffer) => void) | null): void + setDisconnectHandler(handler: ((reason?: unknown) => void) | null): void + } + + export interface PairRecord { + hostId: string + systemBuid: string + hostCertificatePem: string + hostPrivateKeyPem: string + rootCertificatePem: string + rootPrivateKeyPem: string + deviceCertificatePem: string + devicePublicKey: Uint8Array + escrowBag?: Uint8Array + } + + export interface StoredPairRecordPayload { + hostId: string + systemBuid: string + hostCertificatePem: string + hostPrivateKeyPem: string + rootCertificatePem: string + rootPrivateKeyPem: string + deviceCertificatePem: string + devicePublicKey: string + escrowBag: string | null + } + + export interface TlsConnection { + is_handshaking(): boolean + write_plaintext(data: Uint8Array): void + feed_tls(data: Uint8Array): void + take_tls_out(): Uint8Array + take_plain_out(): Uint8Array + free(): void + } + + export interface TlsConnectionFactory { + ensureReady?(): Promise + createConnection(request: { + serverName: string + caCertificatePem: string + certificatePem: string + privateKeyPem: string + }): TlsConnection + } + + export class WebUsbTransport implements WebUsbTransportInstance { + constructor(device: unknown, options?: { logger?: unknown; transferSize?: number }) + readonly isOpen: boolean + static supported(): boolean + static requestAppleDevice(logger?: unknown): Promise + open(): Promise + close(): Promise + send(data: ArrayBuffer): Promise + setDataHandler(handler: ((data: ArrayBuffer) => void) | null): void + setDisconnectHandler(handler: ((reason?: unknown) => void) | null): void + } + + export class DirectUsbMuxClient { + constructor( + transport: WebUsbTransportInstance, + options?: { + log?: (message: string) => void + onStateChange?: () => void + lockdownLabel?: string + tlsFactory?: TlsConnectionFactory + pairRecordFactory?: { + createPairRecord(request: { + devicePublicKey: Uint8Array + hostId: string + systemBuid: string + }): Promise + } + }, + ) + readonly isHandshakeComplete: boolean + readonly isLockdownConnected: boolean + readonly isSessionStarted: boolean + readonly isSessionSslEnabled: boolean + readonly isTlsActive: boolean + readonly isPaired: boolean + loadPairRecord(record: PairRecord | null): void + openAndHandshake(): Promise + connectLockdown(port?: number): Promise + getOrFetchDeviceUdid(): Promise + getOrFetchDeviceName(): Promise + pairDevice(hostId: string, systemBuid: string): Promise + startSession(hostId: string, systemBuid: string): Promise<{ + sessionId: string + enableSessionSsl: boolean + }> + close(): Promise + } + + export const LOCKDOWN_PORT: number + export function installIpaViaInstProxy( + client: DirectUsbMuxClient, + ipaData: Uint8Array, + fileName: string, + onLog?: (message: string) => void, + ): Promise + export function sanitizeIpaFileName(fileName: string): string + export function createHostId(): string + export function createSystemBuid(): string + export function encodeStoredPairRecord(record: PairRecord): StoredPairRecordPayload + export function decodeStoredPairRecord(parsed: StoredPairRecordPayload): PairRecord | null + export function createOpenSslWasmTlsFactory(): TlsConnectionFactory + export function generatePairRecordWithOpenSslWasm(request: { + devicePublicKey: Uint8Array + hostId: string + systemBuid: string + }): Promise +} diff --git a/frontend/src/webmuxd-browser.js b/frontend/src/webmuxd-browser.js new file mode 100644 index 0000000..4f143ed --- /dev/null +++ b/frontend/src/webmuxd-browser.js @@ -0,0 +1,15 @@ +export { WebUsbTransport } from "../../dependencies/webmuxd/src/core/webusb-transport.ts" +export { + DirectUsbMuxClient, + LOCKDOWN_PORT, + installIpaViaInstProxy, + sanitizeIpaFileName, + createHostId, + createSystemBuid, + encodeStoredPairRecord, + decodeStoredPairRecord, +} from "../../dependencies/webmuxd/src/core/imobiledevice-client.ts" +export { + createOpenSslWasmTlsFactory, + generatePairRecordWithOpenSslWasm, +} from "./wasm/openssl-webmuxd.js" diff --git a/frontend/src/webmuxd-shim.d.ts b/frontend/src/webmuxd-shim.d.ts deleted file mode 100644 index 503e106..0000000 --- a/frontend/src/webmuxd-shim.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "webmuxd" { - const webmuxd: Record - export default webmuxd -} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index af07d05..e778faa 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -18,16 +18,22 @@ export default defineConfig({ }, resolve: { alias: { - webmuxd: resolve(repoRootDir, "dependencies/webmuxd/lib/webmuxd.js"), + webmuxd: resolve(frontendDir, "src/webmuxd-browser.js"), }, - preserveSymlinks: true, }, optimizeDeps: { - include: ["@lbr77/anisette-js/browser"], + exclude: [ + "altsign.js", + "@lbr77/anisette-js", + "@lbr77/anisette-js/browser", + "@lbr77/zsign-wasm-resigner-wrapper", + "libcurl.js", + "libcurl.js/bundled", + ], }, build: { commonjsOptions: { - include: [/node_modules/, /\/lib\/webmuxd\.js/, /\/lib\/core\/.*\.js/], + include: [/node_modules/], }, }, }) diff --git a/package.json b/package.json index e1283b2..5d25b09 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,20 @@ "workspaces": [ "frontend", "backend", - "dependencies/webmuxd" + "dependencies/webmuxd", + "wasm/openssl", + "wasm/libcurl-wasm", + "wasm/zsign-wasm" ], "scripts": { "build": "cd dependencies/webmuxd && bun run build", "build:webmuxd": "cd dependencies/webmuxd && bun run build", "build:frontend": "cd frontend && bun run build", "build:backend": "cd backend && bun run check", + "build:wasm": "bun run build:wasm:openssl && bun run build:wasm:libcurl && bun run build:wasm:zsign", + "build:wasm:openssl": "bash scripts/build-wasm-openssl.sh", + "build:wasm:libcurl": "bash scripts/build-wasm-libcurl.sh", + "build:wasm:zsign": "bash scripts/build-wasm-zsign.sh", "lint": "cd dependencies/webmuxd && bun run lint", "test": "cd dependencies/webmuxd && bun run test" } diff --git a/scripts/build-wasm-libcurl.sh b/scripts/build-wasm-libcurl.sh new file mode 100644 index 0000000..fb866f8 --- /dev/null +++ b/scripts/build-wasm-libcurl.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=./wasm-common.sh +source "$SCRIPT_DIR/wasm-common.sh" + +activate_emscripten + +if [[ "$(uname -s)" != "Linux" ]]; then + echo "libcurl-wasm upstream only supports Linux builds." >&2 + echo "Run this script on Linux, or use orb to enter a Linux environment first." >&2 + exit 1 +fi + +cd "$REPO_ROOT/wasm/libcurl-wasm" +bun run build diff --git a/scripts/build-wasm-openssl.sh b/scripts/build-wasm-openssl.sh new file mode 100644 index 0000000..c4b173e --- /dev/null +++ b/scripts/build-wasm-openssl.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=./wasm-common.sh +source "$SCRIPT_DIR/wasm-common.sh" + +OPENSSL_PRECOMPILED="$(resolve_openssl_precompiled_dir || true)" +if [[ -z "${OPENSSL_PRECOMPILED}" ]]; then + echo "OpenSSL precompiled directory not found." >&2 + echo "Set OPENSSL_PRECOMPILED_DIR or OPENSSL_ROOT to a directory containing include/ and lib/." >&2 + exit 1 +fi + +cd "$REPO_ROOT/wasm/openssl" +OPENSSL_ROOT="$OPENSSL_PRECOMPILED" bun run build diff --git a/scripts/build-wasm-zsign.sh b/scripts/build-wasm-zsign.sh new file mode 100644 index 0000000..1e7dac8 --- /dev/null +++ b/scripts/build-wasm-zsign.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=./wasm-common.sh +source "$SCRIPT_DIR/wasm-common.sh" + +activate_emscripten + +OPENSSL_PRECOMPILED="$(resolve_openssl_precompiled_dir || true)" +if [[ -z "${OPENSSL_PRECOMPILED}" ]]; then + echo "OpenSSL precompiled directory not found." >&2 + echo "Set OPENSSL_PRECOMPILED_DIR or OPENSSL_WASM to a directory containing include/ and lib/." >&2 + exit 1 +fi + +cd "$REPO_ROOT/wasm/zsign-wasm" +OPENSSL_WASM="$OPENSSL_PRECOMPILED" bun run build diff --git a/scripts/wasm-common.sh b/scripts/wasm-common.sh new file mode 100644 index 0000000..c310a0e --- /dev/null +++ b/scripts/wasm-common.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +resolve_openssl_precompiled_dir() { + local candidate + + for candidate in \ + "${OPENSSL_WASM:-}" \ + "${OPENSSL_ROOT:-}" \ + "${OPENSSL_PRECOMPILED_DIR:-}" \ + "$REPO_ROOT/openssl-wasm/precompiled" \ + "$REPO_ROOT/wasm/openssl/precompiled" \ + "$REPO_ROOT/wasm/vendor/openssl-wasm/precompiled" \ + "$REPO_ROOT/wasm/openssl-wasm/precompiled" + do + if [[ -n "$candidate" && -d "$candidate" ]]; then + printf '%s\n' "$candidate" + return 0 + fi + done + + return 1 +} + +activate_emscripten() { + if [[ -n "${EMSDK_ENV:-}" ]]; then + if [[ ! -f "${EMSDK_ENV}" ]]; then + echo "EMSDK_ENV does not exist: ${EMSDK_ENV}" >&2 + return 1 + fi + + # shellcheck disable=SC1090 + . "${EMSDK_ENV}" + fi + + if ! command -v emcc >/dev/null 2>&1 || ! command -v em++ >/dev/null 2>&1; then + echo "Emscripten is not available. Install it or set EMSDK_ENV to emsdk_env.sh." >&2 + return 1 + fi +} diff --git a/wasm/openssl/.gitignore b/wasm/openssl/.gitignore index b83d222..941b29c 100644 --- a/wasm/openssl/.gitignore +++ b/wasm/openssl/.gitignore @@ -1 +1,3 @@ /target/ +/binary/ +/dist/ diff --git a/wasm/openssl/Cargo.lock b/wasm/openssl/Cargo.lock index 7bb5a5f..193d86b 100644 --- a/wasm/openssl/Cargo.lock +++ b/wasm/openssl/Cargo.lock @@ -16,9 +16,9 @@ checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" dependencies = [ "find-msvc-tools", "shlex", @@ -53,15 +53,15 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" dependencies = [ "once_cell", "wasm-bindgen", @@ -79,9 +79,9 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "openssl" @@ -150,9 +150,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -237,9 +237,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" dependencies = [ "cfg-if", "once_cell", @@ -250,9 +250,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -260,9 +260,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" dependencies = [ "bumpalo", "proc-macro2", @@ -273,18 +273,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/wasm/openssl/Cargo.toml b/wasm/openssl/Cargo.toml index 8ef2f21..792e981 100644 --- a/wasm/openssl/Cargo.toml +++ b/wasm/openssl/Cargo.toml @@ -5,18 +5,19 @@ edition = "2021" [lib] crate-type = ["cdylib", "rlib"] +path = "rust-src/lib.rs" [build-dependencies] cc = "1" [dependencies] -js-sys = "0.3" +js-sys = "0.3.94" openssl = "0.10.75" openssl-sys = { path = "vendor/openssl-sys" } serde = { version = "1", features = ["derive"] } serde_json = "1" -wasm-bindgen = "0.2.100" -web-sys = { version = "0.3", features = ["Window", "Crypto"] } +wasm-bindgen = "0.2.117" +web-sys = { version = "0.3.94", features = ["Window", "Crypto"] } [patch.crates-io] openssl-sys = { path = "vendor/openssl-sys" } diff --git a/wasm/openssl/build.mjs b/wasm/openssl/build.mjs new file mode 100644 index 0000000..f03950d --- /dev/null +++ b/wasm/openssl/build.mjs @@ -0,0 +1,11 @@ +import { cp, mkdir, rm } from "node:fs/promises" +import { dirname, resolve } from "node:path" +import { fileURLToPath } from "node:url" + +const packageDir = dirname(fileURLToPath(import.meta.url)) +const sourceDir = resolve(packageDir, "src") +const distDir = resolve(packageDir, "dist") + +await rm(distDir, { recursive: true, force: true }) +await mkdir(distDir, { recursive: true }) +await cp(sourceDir, distDir, { recursive: true }) diff --git a/wasm/openssl/build.rs b/wasm/openssl/build.rs index 2cdf208..999945f 100644 --- a/wasm/openssl/build.rs +++ b/wasm/openssl/build.rs @@ -1,6 +1,6 @@ fn main() { cc::Build::new() - .file("src/c_shim/vsnprintf_shim.c") + .file("rust-src/c_shim/vsnprintf_shim.c") .flag_if_supported("-std=c99") .compile("vsnprintf_shim"); } diff --git a/wasm/openssl/build.sh b/wasm/openssl/build.sh index ef450ba..79edcd2 100755 --- a/wasm/openssl/build.sh +++ b/wasm/openssl/build.sh @@ -4,8 +4,42 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" cd "$ROOT_DIR" -OPENSSL_ROOT="/Users/libr/Desktop/Life/browser-apple/openssl-wasm/precompiled" -LLVM_BIN="/opt/homebrew/opt/llvm/bin" +OPENSSL_ROOT="${OPENSSL_ROOT:-${OPENSSL_PRECOMPILED_DIR:-$ROOT_DIR/precompiled}}" +LLVM_BIN="${LLVM_BIN:-}" + +if [[ ! -d "$OPENSSL_ROOT" ]]; then + echo "OpenSSL precompiled directory not found: $OPENSSL_ROOT" >&2 + echo "Set OPENSSL_ROOT or OPENSSL_PRECOMPILED_DIR to a directory containing include/ and lib/." >&2 + exit 1 +fi + +if [[ -z "$LLVM_BIN" ]] && command -v brew >/dev/null 2>&1; then + BREW_LLVM_PREFIX="$(brew --prefix llvm 2>/dev/null || true)" + if [[ -n "$BREW_LLVM_PREFIX" && -x "$BREW_LLVM_PREFIX/bin/clang" ]]; then + LLVM_BIN="$BREW_LLVM_PREFIX/bin" + fi +fi + +if [[ -n "$LLVM_BIN" ]]; then + CLANG_BIN="$LLVM_BIN/clang" + LLVM_AR_BIN="$LLVM_BIN/llvm-ar" + LLVM_RANLIB_BIN="$LLVM_BIN/llvm-ranlib" +else + CLANG_BIN="${CLANG_BIN:-$(command -v clang || true)}" + LLVM_AR_BIN="${LLVM_AR_BIN:-$(command -v llvm-ar || true)}" + LLVM_RANLIB_BIN="${LLVM_RANLIB_BIN:-$(command -v llvm-ranlib || true)}" +fi + +if [[ -z "$CLANG_BIN" || -z "$LLVM_AR_BIN" || -z "$LLVM_RANLIB_BIN" ]]; then + echo "LLVM tools for wasm32-unknown-unknown are not available." >&2 + echo "Set LLVM_BIN or CLANG_BIN/LLVM_AR_BIN/LLVM_RANLIB_BIN." >&2 + exit 1 +fi + +if ! command -v wasm-bindgen >/dev/null 2>&1; then + echo "Missing wasm-bindgen CLI. Install it with: cargo install wasm-bindgen-cli" >&2 + exit 1 +fi export OPENSSL_NO_VENDOR=1 export OPENSSL_STATIC=1 @@ -14,22 +48,18 @@ export OPENSSL_LIB_DIR="$OPENSSL_ROOT/lib" export OPENSSL_INCLUDE_DIR="$OPENSSL_ROOT/include" export OPENSSL_LIBS="ssl:crypto" -export CC_wasm32_unknown_unknown="$LLVM_BIN/clang --target=wasm32-unknown-unknown" -export AR_wasm32_unknown_unknown="$LLVM_BIN/llvm-ar" -export RANLIB_wasm32_unknown_unknown="$LLVM_BIN/llvm-ranlib" +export CC_wasm32_unknown_unknown="$CLANG_BIN --target=wasm32-unknown-unknown" +export AR_wasm32_unknown_unknown="$LLVM_AR_BIN" +export RANLIB_wasm32_unknown_unknown="$LLVM_RANLIB_BIN" echo "[1/2] Building wasm32-unknown-unknown with openssl-rs..." cargo build --release --target wasm32-unknown-unknown -if command -v wasm-bindgen >/dev/null 2>&1; then - echo "[2/2] Generating JS bindings with wasm-bindgen..." - mkdir -p pkg - wasm-bindgen \ - target/wasm32-unknown-unknown/release/openssl_wasm.wasm \ - --out-dir pkg \ - --target web - echo "Done: pkg/ generated." -else - echo "[2/2] wasm-bindgen CLI not found; skipped JS binding generation." - echo "Install via: cargo install wasm-bindgen-cli" -fi +echo "[2/2] Generating JS bindings with wasm-bindgen..." +rm -rf binary +mkdir -p binary +wasm-bindgen \ + target/wasm32-unknown-unknown/release/openssl_wasm.wasm \ + --out-dir binary \ + --target web +echo "Done: binary/ generated." diff --git a/wasm/openssl/package.json b/wasm/openssl/package.json new file mode 100644 index 0000000..625a9fa --- /dev/null +++ b/wasm/openssl/package.json @@ -0,0 +1,28 @@ +{ + "name": "@lbr77/openssl-wasm", + "version": "0.1.0", + "description": "OpenSSL WebAssembly package with stable src/dist/binary layout.", + "type": "module", + "main": "./dist/index.mjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.mjs" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist/**/*", + "binary/**/*", + "README.md" + ], + "scripts": { + "build:binary": "bash ./build.sh", + "build:dist": "bun ./build.mjs", + "build": "bun run build:binary && bun run build:dist", + "clean": "rm -rf dist binary", + "prepack": "bun run build" + } +} diff --git a/wasm/openssl/src/c_shim/vsnprintf_shim.c b/wasm/openssl/rust-src/c_shim/vsnprintf_shim.c similarity index 100% rename from wasm/openssl/src/c_shim/vsnprintf_shim.c rename to wasm/openssl/rust-src/c_shim/vsnprintf_shim.c diff --git a/wasm/openssl/src/lib.rs b/wasm/openssl/rust-src/lib.rs similarity index 99% rename from wasm/openssl/src/lib.rs rename to wasm/openssl/rust-src/lib.rs index a504db3..fb68c30 100644 --- a/wasm/openssl/src/lib.rs +++ b/wasm/openssl/rust-src/lib.rs @@ -38,6 +38,15 @@ static COUNT_STAT: AtomicUsize = AtomicUsize::new(0); static COUNT_OPENDIR: AtomicUsize = AtomicUsize::new(0); static ARC4_FALLBACK_SEED: AtomicUsize = AtomicUsize::new(0x9E37_79B9); +#[no_mangle] +pub static mut errno: c_int = 0; + +#[no_mangle] +pub static CLOCK_REALTIME: c_int = 0; + +#[no_mangle] +pub static _CLOCK_REALTIME: c_int = 0; + #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct PairRecordWasmOut { diff --git a/wasm/openssl/src/index.d.ts b/wasm/openssl/src/index.d.ts new file mode 100644 index 0000000..8c43588 --- /dev/null +++ b/wasm/openssl/src/index.d.ts @@ -0,0 +1,3 @@ +export * from "../binary/openssl_wasm.js" + +export default function ensureOpenSslWasmModuleReady(input?: unknown): Promise diff --git a/wasm/openssl/src/index.mjs b/wasm/openssl/src/index.mjs new file mode 100644 index 0000000..c6d51cb --- /dev/null +++ b/wasm/openssl/src/index.mjs @@ -0,0 +1,17 @@ +import initOpenSslWasm from "../binary/openssl_wasm.js" + +export * from "../binary/openssl_wasm.js" + +let initPromise = null + +/** + * Keep a stable ESM wrapper in `src/` so consumers always import the package + * entry, while the raw wasm-bindgen output stays isolated in `binary/`. + */ +export default async function ensureOpenSslWasmModuleReady(input) { + if (!initPromise) { + initPromise = Promise.resolve(initOpenSslWasm(input)).then(() => undefined) + } + + await initPromise +}