Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 30 additions & 69 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"prettier",
"prettier/@typescript-eslint"
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "tsconfig.json",
"sourceType": "module"
},
"ignorePatterns": ["src/__tests__/**/*"],
"ignorePatterns": [
"src/__tests__/**/*",
"src/remote/transport.ts"
],
"plugins": [
"eslint-plugin-jsdoc",
"eslint-plugin-prefer-arrow",
Expand All @@ -28,65 +30,19 @@
"default": "array"
}
],
"@typescript-eslint/ban-types": [
"error",
{
"types": {
"Object": {
"message": "Avoid using the `Object` type. Did you mean `object`?"
},
"Function": {
"message": "Avoid using the `Function` type. Prefer a specific function type, like `() => void`."
},
"Boolean": {
"message": "Avoid using the `Boolean` type. Did you mean `boolean`?"
},
"Number": {
"message": "Avoid using the `Number` type. Did you mean `number`?"
},
"String": {
"message": "Avoid using the `String` type. Did you mean `string`?"
},
"Symbol": {
"message": "Avoid using the `Symbol` type. Did you mean `symbol`?"
}
}
}
],
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/dot-notation": "error",
"@typescript-eslint/indent": "off",
"@typescript-eslint/member-delimiter-style": [
"off",
{
"multiline": {
"delimiter": "none",
"requireLast": true
},
"singleline": {
"delimiter": "semi",
"requireLast": false
}
}
],
"@typescript-eslint/naming-convention": "error",
"@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-parameter-properties": "off",
"@typescript-eslint/no-unused-expressions": "error",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-var-requires": "error",
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/quotes": "off",
"@typescript-eslint/semi": [
"off",
null
],
"@typescript-eslint/triple-slash-reference": [
"error",
{
Expand All @@ -95,7 +51,6 @@
"lib": "always"
}
],
"@typescript-eslint/type-annotation-spacing": "off",
"@typescript-eslint/unified-signatures": "error",
"arrow-parens": [
"off",
Expand All @@ -114,22 +69,9 @@
"smart"
],
"guard-for-in": "error",
"id-blacklist": [
"error",
"any",
"Number",
"number",
"String",
"string",
"Boolean",
"boolean",
"Undefined",
"undefined"
],
"id-match": "error",
"jsdoc/check-alignment": "error",
"jsdoc/check-indentation": "error",
"jsdoc/newline-after-description": "error",
"linebreak-style": "off",
"max-classes-per-file": [
"error",
Expand All @@ -138,7 +80,7 @@
"max-len": "off",
"new-parens": "off",
"newline-per-chained-call": "off",
"no-bitwise": "error",
"no-bitwise": "off",
"no-caller": "error",
"no-cond-assign": "error",
"no-console": "error",
Expand Down Expand Up @@ -173,9 +115,6 @@
"prefer-const": "error",
"quote-props": "off",
"radix": "error",
"react/jsx-curly-spacing": "off",
"react/jsx-equals-spacing": "off",
"react/jsx-wrap-multilines": "off",
"space-before-function-paren": "off",
"space-in-parens": [
"off",
Expand All @@ -191,6 +130,28 @@
}
],
"use-isnan": "error",
"valid-typeof": "off"
"valid-typeof": "off",
"@typescript-eslint/no-empty-object-type": "error",
"@typescript-eslint/no-unsafe-function-type": "error",
"@typescript-eslint/no-wrapper-object-types": "error",
"jsdoc/tag-lines": [
"error",
"any",
{
"startLines": 1
}
],
"id-denylist": [
"error",
"any",
"Number",
"number",
"String",
"string",
"Boolean",
"boolean",
"Undefined",
"undefined"
]
}
}
}
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: CI

on:
push:
branches: [main, master]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: yarn
- run: yarn install --frozen-lockfile
- run: yarn typecheck
- run: yarn lint
- run: yarn format:check
- run: yarn test
- run: yarn build
18 changes: 9 additions & 9 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,23 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 16
- run: npm ci
- run: npm test
node-version: 20
- run: yarn install --frozen-lockfile
- run: yarn test

publish-npm:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 16
node-version: 20
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: yarn install --frozen-lockfile
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
src/remote/transport.ts
lib/
69 changes: 56 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,59 @@ NPM:

## How it Works

In this repo you will find `webmuxd.ts`. This is a node module that provides the client side implementation of
connecting to the WebUSB API surface and configuring the message pump. You can see a more complete example and
implementation of this by looking at [webmuxd-example](https://github.com/webmuxd/webmuxd-example) and it's
corresponding server component [go-webmuxd](https://github.com/webmuxd/go-webmuxd). The example connects up this
component to a WebSocket and pipes the USB content back and forth to the `go-webmuxd` server, which finally passes
it to `libimobiledevice` via a UNIX socket on disk.
`webmuxd` has three layers, each usable on its own:

* `MobileDevice` finds the usbmux interface on an Apple device over WebUSB, claims it and pumps its bulk endpoints.
* `UsbMux` and `TcpChannel` are a TypeScript port of the host side of `usbmuxd`: version negotiation, the setup
packet, sequence numbers, packet reassembly, and the minimal TCP (SYN / SYN-ACK / ACK, prompt ACKs, window flow
control) that the device expects. With them a page can talk to device services such as `lockdownd` directly.
* `RemoteChannel` connects devices to a server such as [go-webmuxd](https://github.com/hack-different/go-webmuxd)
over a WebSocket, using the protobuf protocol in [`src/remote/transport.proto`](src/remote/transport.proto).

### Talking to the device in the browser

```ts
import MobileDevice, { CONSOLE_LOGGER } from 'webmuxd';

MobileDevice.logger = CONSOLE_LOGGER;
const device = await MobileDevice.selectDevice();
const mux = await device.startMux();

const lockdown = await mux.connect(62078);
lockdown.onData = (data) => console.log('lockdownd replied', data);
lockdown.write(request); // length-prefixed plist
```

### Relaying to a server

```ts
import MobileDevice, { RemoteChannel } from 'webmuxd';

const remote = new RemoteChannel({ url: 'wss://example.com/v1/device', reopen: true, mode: 'raw' });
for (const device of await MobileDevice.getDevices()) {
await remote.bindDevice(device);
}
```

`RemoteChannel` has two modes, chosen per device in the `DeviceConnected` message:

* `raw` (the default) is the original protocol that `go-webmuxd` speaks today: the server runs usbmux and the
browser relays bytes. The browser now reassembles USB transfers so every `DataFromDevice` carries complete mux
packets.
* `mux` runs usbmux in the browser. The server opens channels to device ports with `OpenChannel` and exchanges
stream bytes with `ChannelData`, so framing and ACKs never cross the WebSocket. This needs server support.

The example app in [webmuxd-example](https://github.com/hack-different/webmuxd-example) shows the pieces wired into
a UI.

## Building

If you wish to create your own build it's rather simple:
`yarn run build`

`yarn test`, `yarn lint` and `yarn typecheck` run the same checks as CI. After editing `transport.proto`, regenerate
`src/remote/transport.ts` with `yarn proto` (needs `protoc` on your `PATH`).

## Developing

The simplest way to build, develop and diagnose is using the `XHC20` USB capture on macOS, and optionally `demuxusb`
Expand All @@ -41,14 +82,16 @@ I would love to see it credited if so!

## Roadmap

To make this component "production ready" and less of a proof of concept the following should be completed:
Done:

* Handle all `usbmuxd` framing and ACKs in script to reduce RTT (`UsbMux`, `TcpChannel`)
* Standardize the WebSocket protocol and move it from `webmuxd-example` into `webmuxd`
(`MobileDevice`, `RemoteChannel`, `transport.proto` / `transport.ts`)

Still open, outside this package:

* Handle all `usbmuxd` framing, and ACKs in script to reduce RTT (round trip time)
* Create a standardized WebSocket protocol and move it from `webmuxd-example` to `webmuxd` keeping only the UI
* Move `MobileDevice.ts`
* Move `RemoteChannel.ts`
* Move `transport.proto` / `transport.ts`
* Support `mux` mode in `go-webmuxd`, and switch `webmuxd-example` to depend on this package
* Factor out "common UI elements" to `webmuxd-ui`
* Device Picker
* Device Info
* Improve `go-webmuxd` as a multi-session "meet me" service
* Improve `go-webmuxd` as a multi-session "meet me" service
32 changes: 27 additions & 5 deletions jestconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
{
"preset": "ts-jest",
"testEnvironment": "node",
"roots": [
"<rootDir>/src"
],
"testRegex": "/__tests__/.*\\.test\\.ts$",
"transform": {
"^.+\\.(t|j)sx?$": "ts-jest"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"]
}
"^.+\\.ts$": [
"ts-jest",
{
"tsconfig": {
"types": [
"jest",
"node",
"w3c-web-usb"
],
"lib": [
"es2020",
"dom"
],
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true
}
}
]
}
}
Loading
Loading